var installPage = 'https://chrome.google.com/webstore/detail/ocbokinknmkiofolhjbhccbhkfpplaif';

// Greased MooTools:
// imported via manifest
// end Greased MooTools

// hack to circumvent 'bug' when overriding toString (and others):
// https://mootools.lighthouseapp.com/projects/2706/tickets/651-classtostring-broken-on-122-big-regression
['toString', 'toLocaleString', 'valueOf', 'toSource', 'watch', 'unwatch', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable'].each(function (method) {
	Class.Mutators[method] = $arguments(0);
});

//GM_addStyle('.InlineEditorOpen { background-color: #000000 ! important } ');
GM_addStyle('.UCP_hidden_from_view { display: none; visibility: hidden; }');
GM_addStyle('.UCP_status_column { display: inline; font-size: 14px; font-weight: bold; }');
GM_addStyle('li.UCP_row_hover:hover { min-height: 15px; background-color: lightgrey; }'); // height: loupiote's script makes the li elements very tiny

// Greasemonkey helpers and wrappers:
// imported via manifest
// end Greasemonkey helpers and wrappers

// background page connection
var backgroundPort = chrome.extension.connect({ name: "flickrunifiedcheckplay" });
backgroundPort.onMessage.addListener(function(msg) {
        if (msg.stat == 'error') {
                GM_log("ERROR: " + msg.statusText);
        }
        $(msg.callbackId).fireEvent(msg.callbackEvent, msg);
});
// end background page connection

// version check:
function getVersion(callbackId, callbackEvent) {
        try {
                backgroundPort.postMessage({ name: 'getVersion', callbackEvent: callbackEvent, callbackId: callbackId });
        } catch(e) {
                GM_log("ERROR connecting to the background page: " + e);
        }
}

function showUpdateNotification(data) {
	var onlineVersion = data.onlineVersion;
	var version = data.version;
	var beta = data.beta;

    var color = 'white';
    var updatespan = $('AlesaDams_updates_span');
    if (!$chk(updatespan)) {
	updatespan = new Element('span', {
		id: 'AlesaDams_updates_span',
		// copied from Google++ userscript:
		styles: {
		    'background': '#E0E0E0',
		    padding: '2px 4px',
		    display: 'block',
		    '-moz-background-clip': 'border',
		    '-moz-background-origin': 'padding',
		    '-moz-background-inline-policy': 'continuous',
		    position: 'fixed',
		    opacity: '0.7',
		    'z-index': 1011,
		    bottom: '5px',
		    right: '5px',
		    left: '5px'
		}
	    }).inject($(document).getElement("body"));
    } else {
	updatespan.setStyle('background', '#E0E0E0');
    }
    var bgColor = beta ? 'grey' : 'black';
    var updater = new Element('span', {
	styles: {
		background: bgColor + ' none repeat scroll 0% 0%',
		margin: '2px 5px',
		position: 'relative',
		'float': beta ? 'left' : 'right',
		opacity: beta ? '0.5' : ''
	}
    }).inject(updatespan);
    new Element('a', {
        html: 'UNIFIED Challenges CheckPlay Tool: ' + (beta ? 'beta ' + version : onlineVersion + ' available'),
        href: installPage,
        target: '_blank',
        title:  'to the ' + (beta? 'stable version' : '') + ' install page (opens in new tab)',
        styles: {
            'color': color,
            'text-decoration': 'none'
        }
    }).inject(updater);
	new Element('a', {
		html: beta ? ' (feedback)' : ' (Changes)',
		title: 'opens in new tab',
		href: beta ? 'http://www.flickr.com/groups/unified_checkplay/discuss/72157623600133810/' :
		             'http://www.flickr.com/groups/unified_checkplay/discuss/72157623882744032/',
		styles: {
			'text-decoration': 'none'
		},
		target: '_blank'
	}).inject(updater);
}

function outOfDate(version, onlineVersion) {
        var reVersionMatch      = /(\d+)\.(\d+)\.(\d+)/;
        var onlineVersionParts  = reVersionMatch.exec(onlineVersion);
        var currentVersionParts = reVersionMatch.exec(version);
        var onlineVersionMajor, onlineVersionMinor, onlineVersionBuild;
        //[ onlineVersion, onlineVersionMajor, onlineVersionMinor, onlineVersionBuild ] = onlineVersionParts; 'invalid left-hand side' in Chrome
        onlineVersionMajor = onlineVersionParts[1];
        onlineVersionMinor = onlineVersionParts[2];
        onlineVersionBuild = onlineVersionParts[3];
        var currentVersionMajor, currentVersionMinor, currentVersionBuild;
        //[ currentVersion, currentVersionMajor, currentVersionMinor, currentVersionBuild] = currentVersionParts;
        currentVersionMajor = currentVersionParts[1];
        currentVersionMinor = currentVersionParts[2];
        currentVersionBuild = currentVersionParts[3];
        // first check major: important update! => rewrite, flickr updates, greasemonkey updates
        if (parseInt(onlineVersionMajor, 10) > parseInt(currentVersionMajor, 10)) {
            return true;
        } else if (parseInt(onlineVersionMajor, 10) === parseInt(currentVersionMajor, 10)) { // we don't want to downgrade
            // minor version update => new functionality
            if (parseInt(onlineVersionMinor, 10) > parseInt(currentVersionMinor, 10)) {
                return true;
            } else if (parseInt(onlineVersionMinor, 10) === parseInt(currentVersionMinor, 10)) { // we don't want to downgrade
                // build version update => bugfixes
                if (parseInt(onlineVersionBuild, 10) > parseInt(currentVersionBuild, 10)) {
                    return true;
                }
            }
        }
	return false;
}

function getThreadInfo(data) {
        var threadId = data.threadId;
        var callback = data.callback;

        try {  
                new Request({
                        url: 'http://www.flickr.com',
                        onSuccess: function (responseText, responseXML) {
                                    try {
                                        var result;
                                        try {
                                            result = JSON.parse(responseText);
                                        } catch (e) {
                    				GM_log("error parsing JSON reply for thread " + threadId + " (" + e + "): '" + responseText);
		    				callback({ threadId: threadId, success: false, message: e });
						return;
                                        }
                                        if (result.stat === 'fail') {
                                                callback({ threadId: threadId, success: false, message: result.message });
                                        } else {
                                                callback({ threadId: threadId, success: true, message: result.topic.message._content });
                                        }
                                    } catch (e) {
                                        GM_log("ERROR processing result: " + e);
                                        callback({ threadId: threadId, success: false, message: "ERROR processing result: " + e });
                                    }
                                },
                                onFailure: function (response) {
                                        GM_log("error: " + response.statusText);
                                        callback({ threadId: threadId, success: false, message: response.statusText });
                                }
                }).get('/services/rest', {
                        api_key: GM_getPublicKey(), // as long as we use the function in checkVersion only
                        auth_hash: GM_getAuthHash(),
                        auth_token: GM_getAuthToken(),
                        format: 'json',
                        method: 'flickr.groups.discuss.topics.getInfo',
                        nojsoncallback: 1,
                        topic_id: threadId
                });
        } catch (e) {
                callback({ threadId: threadId, success: false, message: 'Exception: ' + e });
        }
}

function checkVersion(version) {
try {
    var lastVersionCheckTime = GM_getValue("lastVersionCheckTime");
    var elapsedtime = (60 * 60 * 12 + 1) * 1000;
    var now = new Date();
    if ($chk(lastVersionCheckTime)) {
        elapsedtime = now.getTime() - lastVersionCheckTime;
    }
    if (elapsedtime / 1000 > 60 * 60 * 12) { //more then 12h ago
    	getThreadInfo({ threadId: '72157623600133810', callback: function(retval) {
			var success = retval.success;
			var message = retval.message;
			if (!success) {
				GM_log("error getting version info: " + message);
				GM_deleteValue("onlineVersion");
				GM_deleteValue("lastVersionCheckTime");
				return;
			}
			var onlineVersion = message.split(/<i>current stable extension version:\s*/)[1].split(/<\/i>/)[0];
			GM_setValue("onlineVersion", onlineVersion);
        		GM_setValue("lastVersionCheckTime", (new Date()).getTime().toString());
		}
	});
        GM_setValue("lastVersionCheckTime", now.getTime().toString());
    }

    var onlineVersion = GM_getValue("onlineVersion");
    if ($chk(onlineVersion)) {
        var updateAvailable = outOfDate(version, onlineVersion);
        if (updateAvailable) {
            showUpdateNotification({ onlineVersion: onlineVersion });
        } else if (version != onlineVersion) { // not out of date, but different version => beta
	    showUpdateNotification({ onlineVersion: onlineVersion, version: version, beta: true });
        }
    }
} catch (e) {
	GM_log("ERROR checking version: " + e);
}
}
// end of version check

// -----------

var CPStartTime = new Date();

var UCPprefix = 'UCP';
var UCPGroupConfig = new Class({
    Implements: [Options],
    options: {
        challengeDefinitions: {},
        groupLimit: -1,
        states: {},
        allowsPhotoInAnnouncement: false,
        nonPhotoImages: {},
        groupLimitLabelAddon: "",
        skipFirstReply: false, // FavesContest shows last winning score in first reply => Finished
        skipFirstTwoRepliesForVotes: false, // FavesContest have a second reply '... 25-50 faves...', recognized as a vote
        mandatoryGroupLabels: false,
        automaticVoteStart: true,
        languageOverrides: {},
        legacyLabels: null,
        groupname: null, // the part of the Flickr URL
        name: null, // the human name
        groupId: null,
        awardWinnersGroup: false, // only used in UCP Admin
        primaryGroup: null, // only used in UCP Admin
	bumpMessage: "", //<i>This is a comment to bump the thread to the top of the list</i>"
        workflow: undefined,
	usesTimeConstraints: false,
	challengeNumberingRegExp: null, // Admin only
	challengeNumberingLabel: null,  // for challenge groups that don't number: check for name format
	similarThemesRegExp: null,      // Admin only
        debug: false
    },
    initialize: function (options) {
        this.setOptions(options);
        // override states with RegExp's
        [ 'open', 'waitingForEntries', 'closed', 'vote', 'expired', 'voided' ].each(function (state) {
            // override states with RegExp's
            if ($chk(options.states[state + 'RegExp'])) {
                //GM_log("creating " + state + " regexp");
                this.options.states[state] = 
                    new RegExp(options.states[state + 'RegExp'].expression, options.states[state + 'RegExp'].flags);
            }
            if (!$chk(this.options.states[state])) {
                GM_log("!!! no definition for state '" + state + "' !!!");
            }
        }, this);
        if (this.options.debug == true || this.options.debug == 'true') {
            // force re-read
            GM_setValue(UCPprefix + ".groupConfig.lastReadTime." + this.groupname(), 1);
        }
    },
    // accessors
    groupname: function () {
        return this.options.groupname;
    },
    challengeDefinitions: function () {
        return this.options.challengeDefinitions;
    },
    groupLimit: function () {
        return this.options.groupLimit;
    },
    groupLimitLabelAddon: function () {
        return this.options.groupLimitLabelAddon;
    },
    states: function () {
        return this.options.states;
    },
    legacyLabels: function () {
        return this.options.legacyLabels;
    },
    skipFirstReply: function () {
        return this.options.skipFirstReply;
    },
    skipFirstTwoRepliesForVotes: function () {
        return this.options.skipFirstTwoRepliesForVotes;
    },
    hasLegacyLabels: function () {
        return $chk(this.options.legacyLabels);
    },
    mandatoryGroupLabels: function () {
        return this.options.mandatoryGroupLabels;
    },
    automaticVoteStart: function () {
        return this.options.automaticVoteStart;
    },
    languageOverrides: function () {
        return this.options.languageOverrides;
    },
    allowsPhotoInAnnouncement: function () {
        return this.options.allowsPhotoInAnnouncement;
    },
    nonPhotoImages: function () {
        return this.options.nonPhotoImages;
    },
    groupId: function () {
        return this.options.groupId;
    },
    reExcludeMatch: function () {
        if ($chk(this.reExcludeMatchRegExp)) {
            return this.reExcludeMatchRegExp;
        }

        if (!$chk(this.options.excludes) || !$chk(this.options.excludes.matches) || !$chk(this.options.excludes.indexes)) {
            return /[^.]/; // match nothing
        }

        if ($chk(this.options.excludes.matches.reMatchRegExp)) { // only one
            this.reExcludeMatchRegExp = new RegExp(this.options.excludes.matches.reMatchRegExp.expression,
                                                    this.options.excludes.matches.flags);
            return this.reExcludeMatchRegExp;
        }

        var reMatches = [];
        for (var name in this.options.excludes.matches) {
            if (this.options.excludes.matches.hasOwnProperty(name)) {
                reMatches.push(this.options.excludes.matches[name].replace(/&lt;/g, '<').replace(/&gt;/g, '>'));
            }
        }
//        GM_log("creating regexp with '" + reMatches.join("|") + "'");
        return new RegExp(reMatches.join("|"), "ig");
    },
    excludeReplyIndexes: function () {
        if (!$chk(this.options.excludes) || !$chk(this.options.excludes.matches) || !$chk(this.options.excludes.indexes)) {
            return [];
        }
        var retval = [];
        for (var index in this.options.excludes.indexes) {
            if (this.options.excludes.indexes.hasOwnProperty(index)) {
                retval.push(this.options.excludes.indexes[index]);
            }
        }
        //GM_log("debug: " + retval);
        return retval;
    },
    checkPlayerEntries: function (args) {
        if (this.groupLimit() <= 0) {
            return;
        }
        // TODO: for challenge groups where there is no group limit, but there is a limit for some specific challenges,
        // use the phrase "You have reached the maximum play limit in the capped competitions" (Fotocompetitions group)
        if (args.entries === this.groupLimit()) {
	    reportStatus("UCheckPlayNG: You entered " + args.entries + 
                " challenges and have reached your maximum play limit! " + 
                ($chk(this.groupLimitLabelAddon()) ? this.groupLimitLabelAddon() : ""), 'warning');
        }
        if (args.entries > this.groupLimit()) {
	    reportStatus("UCheckPlayNG: You entered over " + this.groupLimit() + 
                " challenges and are thus breaking the rules! Please remove your latest entry! " + 
                ($chk(this.groupLimitLabelAddon()) ? this.groupLimitLabelAddon() : ""), 'error');
        }
    },
    extractChallengeDefinition: function (challengeName) {
        // do not run .each()! must stop after the first match: priorities
        if (!$chk(this.challengeDefinitions())) {
            GM_log("no challenge definitions for this group");
            return new UCPChallengeDefinition({
                scoreType: "UNKNOWN"
            });
        }
        for (var name in this.challengeDefinitions()) {
            if (this.challengeDefinitions().hasOwnProperty(name)) {
                var challengeDef = this.challengeDefinitions()[name];
                // TMOACG still uses reName (and others)
                var nameIndication = challengeDef.reName; // challengeDef is a simple JSON object
                if ($chk(challengeDef.reNameRegExp)) {
                    if (!$chk(challengeDef.reNameRegExpObject)) {
                        challengeDef.reNameRegExpObject = 
                                new RegExp(challengeDef.reNameRegExp.expression, challengeDef.reNameRegExp.flags);
                    }
                    nameIndication = challengeDef.reNameRegExpObject;
                }
                if (!$chk(nameIndication)) {
                    GM_log("!! missing reName, or reNameRegExp !!");
                } else {
                    var theMatch = challengeName.match(nameIndication);
                    // DEBUG
                    //GM_log("comparing "+challengeName+" with "+nameIndication+": "+theMatch);
                    if ($chk(theMatch)) {
                        // inject name
                        challengeDef.name = name;
                        return new UCPChallengeDefinition(challengeDef);
                    }
                }
            }
        }
        // none found!
        GM_log("NOT FOUND: " + challengeName + " not found in definitions");
        return new UCPChallengeDefinition({
            scoreType: "UNKNOWN"
        });
    },
    skipChallenge: function (challengeName) {
        if (challengeName.match(this.states().closed))  {
            return true;
        }
        if (challengeName.match(this.states().expired)) {
            return true;
        }
        if (challengeName.match(this.states().voided))  {
            return true;
        }
        for (var name in this.challengeDefinitions()) {
            if (this.challengeDefinitions().hasOwnProperty(name)) {
                var challengeDef = this.challengeDefinitions()[name];
                var specialName = ( $chk(challengeDef.reNameRegExpObject) ? challengeDef.reNameRegExpObject : challengeDef.reName);
                if (challengeName.match(specialName)) {
                    return false;
                }
            }
        }
        if (challengeName.match(this.states().open))    {
            return false;
        }
        if (challengeName.match(this.states().waitingForEntries)) {
            return false;
        }
        if (challengeName.match(this.states().vote))    {
            return false;
        }
        return true;
    },
    usesTimeConstraints: function () {
	return this.options.usesTimeConstraints == true || this.options.usesTimeConstraints == 'true';
    },
    hasTimeConstrainedChallenges: function () {
        for (var name in this.challengeDefinitions()) {
            if (this.challengeDefinitions().hasOwnProperty(name)) {
                var challengeDef = this.challengeDefinitions()[name];
                // challengeDef is a simple JSON object
		if ($chk(challengeDef.usesTimeConstraints)) {
			return true;
		}
	    }
	}
	return false;
    }
});
var global_group_config = null;

var UCPGroupConfigReader = new Class({
    timeBetweenReads: 12 * 60 * 60 * 1000, // 12 hours
    groupListingURL: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623452533109/',
    initialize: function () {
    },
    checkForUpdates: function (data) {
	var group_name = data.group_name;
	var callback = data.callback;

    	if (!$chk(GM_getValue(UCPprefix + ".groupConfig.list"))) {
		this.readGrouplistURL(function (result) {
			if (!result.success) {
				callback({ success: false, message: 'unable to read group list: ' + result.message });
				return;
			}
			if (!$chk(GM_getValue(UCPprefix + ".groupConfig.list"))) { // sanity check
				GM_log("internal error: no groupConfig list created; aborting");
				callback({ success: false, message: "internal error: no groupConfig list created; aborting" });
				return;
			}
			this.checkForUpdates(data);
		}.bind(this));
		return; // the recursive call above will get to the block below
	}
        if ($chk(GM_getValue(UCPprefix + ".groupConfig." + group_name))) {
            var lastReadTime = GM_getValue(UCPprefix + ".groupConfig.lastReadTime." + group_name);
            var now = new Date().getTime();
            var elapsedTime = $chk(lastReadTime) ? now - lastReadTime : this.timeBetweenReads + 1;
            if (elapsedTime > this.timeBetweenReads) {
                GM_log("updating " + group_name + " definitions");
                this.readGroupConfigURL(group_name, callback);
            } else {
		callback({ success: true });
	    }
        } else {
            GM_log("updating " + group_name + " definitions");
            this.readGroupConfigURL(group_name, callback);
        }
    },
    createGroupConfig: function (group_name, callback) {
        GM_log("reading config for '" + group_name + "'");
        var storedConfig = GM_getValue(UCPprefix + ".groupConfig." + group_name);
        if ($chk(storedConfig) && storedConfig.match('groupId')) { // test on groupId for versions prior to CL v0.0.7
            try {
                var retval = new UCPGroupConfig(GM_getObject(UCPprefix + ".groupConfig." + group_name));
		callback({ success: true, groupConfig: retval });
		return;
            } catch (e) {
                // parse error?
		GM_log("error parsing group config for group '" + group_name + "'; retrying");
                GM_deleteValue(UCPprefix + ".groupConfig." + group_name);
            }
        }
        // if not available, read from URL
        // make sure the group list is read also: a new group should not have to hit F5 twice, just to get support!
        GM_deleteValue(UCPprefix + ".groupConfig.list");
        this.readGroupConfigURL(group_name, function (retval) {
		if (!retval.success) {
			callback(retval);
			return;
		}
		storedConfig = GM_getValue(UCPprefix + ".groupConfig." + group_name);
		if ($chk(storedConfig)) {
			try {
				var retval = new UCPGroupConfig(GM_getObject(UCPprefix + ".groupConfig." + group_name));
				callback({ success: true, groupConfig: retval });
			} catch (e) {
				// parse error?
				GM_deleteValue(UCPprefix + ".groupConfig." + group_name);
				callback({ success: false, message: e });
			}
			return;
		}
		callback({ success: false, message: "did not find definitions for " + group_name });
	});
    },
    readGroupConfigURL: function (group_name, callback) {
        this.groupList(function(retval) {
		if (!retval.success) {
			callback(retval);
			return;
		}
		var groupList = retval.groupList;
		if (!$chk(groupList[group_name]) || !$chk(groupList[group_name].definitions)) {
		    callback({ success: false, message: 'no definitions found for group ' + group_name });
		    return;
		}
		var groupUrl = groupList[group_name].definitions;
		//GM_log("reading group definitions for '" + group_name + "' (" + groupList[group_name].groupId + ") from '" + groupUrl + "'");
		getThreadInfo({ threadId: groupUrl.match(/.*flickr.com\/groups\/[^\/]+\/discuss\/(\d+)/)[1],
		    callback: function (retval) {
			if (!retval.success) {
				callback(retval);
				return;
			}
			var groupConfiguration = retval.message
						    .trim()
						    .replace(/[\n\r]/g, '') // Flickr changes
						    .replace(/\"/g, '&quot;')
						    .replace(/\\\'/g, '&#39;') // replace \' with its html number
						    .replace(/\'{2}/g, "\"\"")
						    // make the definitions strict JSON (use ' instead of " -> no &quot;)
						    .replace(/\'([^\']+)\'/g, '\"$1\"') // replace ' with " for valid JSON
						    .replace(/&lt;/g, '<')
						    .replace(/&gt;/g, '>')
						    // reset
						    .replace(/&quot;/g, "\'");
			//GM_log("groupConfiguration: " + groupConfiguration);
			var onlineConfig;
			try {
			    onlineConfig = JSON.parse(groupConfiguration);
			    // make the definitions json-valid (use ' instead of " -> no &quot;)
			} catch (e) {
			    GM_log("json error in group config def: " + e); // JSON can't handle 'reName': /^d.../
			    GM_log("onlineConfig: " + groupConfiguration);
			    // Chrome only uses JSON!!
			    try {
				onlineConfig = eval('(' + groupConfiguration + ')');
			    } catch (f) {
				GM_log("error evaluating onlineConfig: " + e);
				GM_log("onlineConfig: " + groupConfiguration);
				callback( { success: false, message: 'error evaluating onlineConfig: ' + e } );
				return;
			    }
			}
			//GM_log("preparing default states");
			// reset defaults for non-defined states
			// TODO: document
			var defaultStates = {
			    open: "OPEN", // no photos yet
			    waitingForEntries: "OPEN", // at least one photo; some groups use "ON HOLD", ..
			    vote: /VOTE/i, // some groups use "VOTING", ..
			    closed: "CLOSED", // 
			    expired: "EXPIRED",
			    voided: "VOIDED"
			};
			// inject group list items
			for (var item in groupList[group_name]) {
			    if (groupList[group_name].hasOwnProperty(item)) {
				onlineConfig[item] = groupList[group_name][item];
			    }
			}
			if (!$chk(onlineConfig.states)) {
			    onlineConfig.states = defaultStates;
			    // special case: vote is defined as a regexp in the defaults => Chrome won't have a vote state!
			    onlineConfig.states.voteRegExp = {
				expression: 'VOTE',
				flags: 'i'
			    };
			} else {
			// warning: special case: vote is defined as a regexp in the defaults!
			    for (var state in defaultStates) {
				if (defaultStates.hasOwnProperty(state)) {
				    if (!$chk(onlineConfig.states[state])) {
					if (state === 'vote' && !$chk(onlineConfig.states['voteRegExp'])) {
					    onlineConfig.states.voteRegExp = {
						expression: 'VOTE',
						flags: 'i'
					    };
					} else {
					    onlineConfig.states[state] = defaultStates[state];
					}
				    }
				}
			    }
			}
			GM_storeObject(UCPprefix + ".groupConfig." + group_name, onlineConfig);
			GM_setValue(UCPprefix + ".groupConfig.lastReadTime." + group_name, new Date().getTime().toString());
			callback( { success: true } );
		    }
		});
	});
    },
    readGrouplistURL: function (callback) {
        //GM_log("reading group list");
    	getThreadInfo({ threadId: this.groupListingURL.match(/.*flickr.com\/groups\/[^\/]+\/discuss\/(\d+)/)[1],
		callback: function(retval) {
			var success = retval.success;
			var message = retval.message;
			if (!success) {
				callback(retval);
				return;
			}
			var groupList = message.trim().replace(/[\n\r]/g, '')
                                            // make the definitions strict JSON (use ' instead of " -> no &quot;)
                                            .replace(/\'([^\']+)\'/g, '\"$1\"'); // replace ' with " for valid JSON
                	var groups = {};
			try {
			    groups = JSON.parse(groupList);
			} catch(e) {
			    GM_log("json error in group list: " + e);
			    GM_log("groupList: " + groupList);
			    try {
				groups = eval('(' + groupList + ')');
			    } catch (f) {
				GM_log("error parsing groupList result: " + e);
				GM_log("groupList: " + groupList);
				callback( { success: false, message: 'error parsing groupList result: ' + e });
				return;
			    }
			}
			// inject groupname
			$each(groups, function (value, key) {
			    value.groupname = key;
			});
			//ucpStoredGrouplist = groups;
			//GM_log("storing group list");
			GM_storeObject(UCPprefix + ".groupConfig.list", groups);
			callback( { success: true } );
		}
	});
    },
    groupList: function (callback) {
        if ($chk(GM_getValue(UCPprefix + ".groupConfig.list"))) {
            //GM_log("working with storage value, and update in background");
            try {
                var groupList = GM_getObject(UCPprefix + ".groupConfig.list");
		callback({ success: true, groupList: groupList });
		return;
            } catch (e) { // parse error?
		GM_log("error parsing group config list; retrying");
                GM_deleteValue(UCPprefix + ".groupConfig.list");
                //return null; continue: reread
            }
        }
        //GM_log("reading list url");
        this.readGrouplistURL(function(retval) {
		if (!retval.success) {
                        callback(retval);
                        return;
                }
		//GM_log("returning stored dict: '" + ucpStoredGrouplist + "'");
		try {
		    groupList = GM_getObject(UCPprefix + ".groupConfig.list");
		    callback({ success: true, groupList: groupList });
		} catch (e) { // parse error?
			GM_log("error parsing group config list; aborting");
			GM_deleteValue(UCPprefix + ".groupConfig.list");
			callback({ success: false, message: 'error parsing group list: ' + e });
		}
	});
    }
});

var UCPChallengeDefinition = new Class({
    Implements: [Options],
    options: {
        name: undefined,
        reName: /.*/,
        reNameRegExp: undefined,
        neededPhotos: -1,
        neededScore: -1,
        scoreType: undefined,
        countsToLimit: false,
        limitPerPlayer: -1,
        playerVoting: 'mayvote',
        scoresAdded: true,
        iconChallenge: false,
	thumbnailView: false,
	usesTimeConstraints: undefined,
	postDeadline: undefined,
	voteDeadline: undefined,
	equalWeights: false,
	workflow: undefined // admin script only
    },
    initialize: function (options) {
        this.setOptions(options);
    },
    name: function () {
        return this.options.name;
    },
    reName: function () {
        if ($chk(this.options.reNameRegExp)) {
            return this.options.reNameRegExp;
        }
        return this.options.reName;
    },
    neededPhotos: function () {
        return this.options.neededPhotos;
    },
    setNeededPhotos: function (n) {
        this.options.neededPhotos = n;
    },
    neededScore: function () {
        return this.options.neededScore;
    },
    setNeededScore: function (n) {
        this.options.neededScore = n;
    },
    scoreType: function () {
        return this.options.scoreType;
    },
    setScoreType: function (s) {
        this.options.scoreType = s;
    },
    countsToLimit: function () {
        return this.options.countsToLimit;
    },
    setCountsToLimit: function (b) {
        this.options.countsToLimit = b;
    },
    limitPerPlayer: function () {
        return this.options.limitPerPlayer;
    },
    setLimitPerPlayer: function (n) {
        this.options.limitPerPlayer = n;
    },
    playerVoting: function () {
        return this.options.playerVoting;
    },
    setPlayerVoting: function (p) {
        this.options.playerVoting = p;
    },
    scoresAdded: function () {
        return this.options.scoresAdded;
    },
    setScoresAdded: function (s) {
        this.options.scoresAdded = s;
    },
    iconChallenge: function () {
        return this.options.iconChallenge;
    },
    setIconChallenge: function (b) {
        this.options.iconChallenge = b;
    },
    thumbnailView: function() {
    	return this.options.thumbnailView;
    },
    setThumbnailView: function(b) {
    	this.options.thumbnailView = b;
    },
    usesTimeConstraints: function () {
	return this.options.usesTimeConstraints;
    },
    setUsesTimeConstraints: function (u) {
    	this.options.usesTimeConstraints = u;
    },
    postDeadline: function () {
    	return this.options.postDeadline;
    },
    setPostDeadline: function (deadline) {
    	this.options.postDeadline = deadline;
    },
    voteDeadline: function () {
    	return this.options.voteDeadline;
    },
    setVoteDeadline: function (deadline) {
    	this.options.voteDeadline = deadline;
    },
    equalWeights: function() {
    	return this.options.equalWeights;
    },
    toString: function () {
        return [ 'reName: ' + this.reName(), 
                 'neededPhotos: ' + this.neededPhotos(), 
                 'neededScore: ' + this.neededScore(), 
                 'scoreType: ' + this.scoreType() ].join(","); 
    },
    clone: function () {
        return new UCPChallengeDefinition(this.options);
    },
    nonChallengeType: function () {
        switch (this.scoreType()) {
        case "CHAT":
        case "SHOWROOM":
        case "GAME":
        case "MEETANDGREET":
        case "INFO":
        case "UNKNOWN":
            return true;
        default:
            return false;
        }
        return true;
    },
    readChallengeDefinitionOverrides: function (announcementNode) {
        var overrides = announcementNode.getElements('img[alt*=UCPoverride]');
        var overridesDefined = false;
        if ($chk(overrides) && overrides.length > 0) {
            overrides.each(function (overrideImg) {
                var override = overrideImg.alt;

                var photosOverrideMatch        = override.match(/:photos:(-{0,1}\d+)$/);
                if (photosOverrideMatch) {
                    var neededPhotosOverride = parseInt(photosOverrideMatch[1], 10);
                    if (this.neededPhotos() !== neededPhotosOverride) {
                        overridesDefined = true;
                        this.setNeededPhotos(neededPhotosOverride);
                        if (this.neededPhotos() < 0) {
                            this.setNeededPhotos(65535);
                        }
                    }
                }
                var votesOverrideMatch         = override.match(/:votes:(-{0,1}\d+)$/);
                if (votesOverrideMatch) {
                    var neededScoreOverride = parseInt(votesOverrideMatch[1], 10);
                    if (this.neededScore() !== neededScoreOverride) {
                        overridesDefined = true;
                        this.setNeededScore(neededScoreOverride);
                        if (this.neededScore() < 0) {
                            this.setNeededScore(65535);
                        }
                    }
                }
                var scoreTypeOverrideMatch     = override.match(/:type:(.*)$/);
                if (scoreTypeOverrideMatch) {
                    var scoreTypeOverride = scoreTypeOverrideMatch[1];
                    if (this.scoreType() !== scoreTypeOverride) {
                        overridesDefined = true;
                        this.setScoreType(scoreTypeOverride);
                    }
                }
                var maxPhotosOverrideMatch     = override.match(/:max:(-{0,1}\d+)/);
                if (maxPhotosOverrideMatch) {
                    var limitPerPlayerOverride = parseInt(maxPhotosOverrideMatch[1], 10);
                    if (this.limitPerPlayer() !== limitPerPlayerOverride) {
                        overridesDefined = true;
                        this.setLimitPerPlayer(limitPerPlayerOverride);
                    }
                }
                var playerVotingOverrideMatch  = override.match(/:voting:(n\a|mustvote|mayvote|maynotvote)$/);
                if (playerVotingOverrideMatch) {
                    var playerVotingOverride = playerVotingOverrideMatch[1];
                    if (this.playerVoting() !== playerVotingOverride) {
                        overridesDefined = true;
                        this.setPlayerVoting(playerVotingOverride);
                    }
                }
                var countsToLimitOverrideMatch = override.match(/:grouplimit:(true|false)$/);
                if (countsToLimitOverrideMatch) {
                    var countsToLimitOverride = (countsToLimitOverrideMatch[1] === "true");
                    if (this.countsToLimit() !== countsToLimitOverride) {
                        overridesDefined = true;
                        this.setCountsToLimit(countsToLimitOverride);
                    }
                }
                var scoresAddedOverrideMatch   = override.match(/:added:(true|false)$/);
                if (scoresAddedOverrideMatch) {
                    var scoresAddedOverride = (scoresAddedOverrideMatch[1] === "true");
                    if (this.scoresAdded() !== scoresAddedOverride) {
                        overridesDefined = true;
                        this.setScoresAdded(scoresAddedOverride);
                    }
                }
                var iconChallengeOverrideMatch   = override.match(/:icons:(true|false)$/);
                if (iconChallengeOverrideMatch) {
                    var iconChallengeOverride = (iconChallengeOverrideMatch[1] === "true");
                    if (this.iconChallenge() !== iconChallengeOverride) {
                        overridesDefined = true;
                        this.setIconChallenge(iconChallengeOverride);
                    }
                }
                var thumbnailViewOverrideMatch = override.match(/:thumbnailview:(true|false)$/);
                if (thumbnailViewOverrideMatch) {
                    var thumbnailViewOverride = (thumbnailViewOverrideMatch[1] === "true");
                    if (this.thumbnailView() !== thumbnailViewOverride) {
                        overridesDefined = true;
                        this.setThumbnailView(thumbnailViewOverride);
                    }
                }
		var postDeadlineDefinitionMatch = override.match(/:postdeadline:(\d+)$/);
		if (postDeadlineDefinitionMatch) {
		    var postDeadlineDefinition = postDeadlineDefinitionMatch[1];
		    this.setPostDeadline(postDeadlineDefinition);
		}
		var voteDeadlineDefinitionMatch = override.match(/:votedeadline:(\d+)$/);
		if (voteDeadlineDefinitionMatch) {
		    var voteDeadlineDefinition = voteDeadlineDefinitionMatch[1];
		    this.setVoteDeadline(voteDeadlineDefinition);
		}
            }, this);
        }
        return overridesDefined;
    }
});

function fetchGroupAdministratorOrModerator(user_nsid, callback) { // removed from UCPGroupConfig to align with admin script
	var groupId = global_group_config.groupId();

	var group_name = global_group_config.groupname();
        var adminOrMod = GM_getValue("UCP.groupAdminOrMod." + group_name + "." + user_nsid);
        var lastReadTime = GM_getValue("UCP.groupAdminOrMod." + group_name + "." + user_nsid + ".lastReadTime");
	var elapsedTime = ($chk(lastReadTime) ? new Date().getTime() - parseInt(lastReadTime,10) : 60 * 60 * 1000 + 1);
	if (adminOrMod != undefined && elapsedTime < 60 * 60 * 1000) {
		callback({ success: true, adminOrMod: adminOrMod == true || adminOrMod == 'true' });
		return;
	}
        
	var magisterLudi = GM_getPrivateKey();
        var apiData = {
            api_key: magisterLudi,
            auth_hash: GM_getAuthHash(),
            auth_token: GM_getAuthToken(),
            format: 'json',
            nojsoncallback: 1,
            method: 'flickr.groups.members.getList',
            group_id: groupId,
            membertypes: '3,4',
            per_page: 500 // one page should be sufficient?
        };
        new Request({
            url: "http://www.flickr.com/",
            onFailure: function (response) {
		GM_log("failed reading members from group: " + response.statusText);
		callback({ success: false, message: response.statusText });
            },
            onSuccess: function (responseText, responseXML) {
                var result;
		var adminOrMod = false;
                try {
                    result = JSON.parse(responseText);
                } catch (e) {
                    result = eval('(' + responseText + ')');
                }
                if (result.stat === 'fail') {
		    GM_log("failed reading members from group: " +result.code + " - " + result.message);
		    callback({ success: false, message: result.message });
                    return;
                }
                var members = result.members;
                $each(members.member, function (member) {
                    if (member.nsid === user_nsid) {
                        adminOrMod = true;
                    }
                });
                GM_setValue("UCP.groupAdminOrMod." + group_name + "." + user_nsid, adminOrMod);
                GM_setValue("UCP.groupAdminOrMod." + group_name + "." + user_nsid + ".lastReadTime", 
                            new Date().getTime().toString());
		callback({ success: true, adminOrMod: adminOrMod });
            }
        }).get("/services/rest", apiData);
    }

var ucpDialogStyle = {
    background: '#BFBFBF',
    '-moz-border-radius': '1em',
    '-webkit-border-radius': '1em',
    '-khtml-border-radius': '1em',
    'border-radius': '1em',
    border: 'grey solid 1px',
    'text-align': 'left'
};

var UCPLanguage = new Class({
    Implements: Options,
    options: {
        name: undefined,
        titles: undefined,
        labels: undefined
    },
    initialize: function (options) {
        this.setOptions(options);
	if (this.options.labels.voted && this.options.labels.voted.match(/icon_check.png/)) {
		this.options.labels.voted = '<img src="' + images.defaultCheck + '" style="max-height: 16px;"/>';
	}
	if (this.options.labels.updating && this.options.labels.updating.match(/pulser2.gif/)) {
		this.options.labels.updating = '<img src="' + images.updating + '" width="21" height="10" />';
	}
    },
    name: function () {
        return this.options.name;
    },
    titles: function () {
        return this.options.titles;
    },
    labels: function () {
        return this.options.labels;
    },
    useLegacyLabels: function (legacyLabels) {
        for (var label in legacyLabels) {
            if (legacyLabels.hasOwnProperty(label)) {
                this.options.labels[label] = legacyLabels[label];
            }
        }
    },
    useLanguageOverrides: function (languageOverrides) {
        if ($chk(languageOverrides)) {
            if ($chk(languageOverrides.titleOverrides)) {
                var titleOverrides = languageOverrides.titleOverrides;
                for (var titleOverride in titleOverrides) {
                    if (titleOverrides.hasOwnProperty(titleOverride)) {
                        this.options.titles[titleOverride] = this.options.titles[titleOverrides[titleOverride]];
                    }
                }
            }
            if ($chk(languageOverrides.labelOverrides)) {
                var labelOverrides = languageOverrides.labelOverrides;
                for (var labelOverride in labelOverrides) {
                    if (labelOverrides.hasOwnProperty(labelOverride)) {
                        this.options.labels[labelOverride] = this.options.labels[labelOverrides[labelOverride]];
                    }
                }
            }
        }
    }
});

var ucpLanguages = [];

var UCPLanguageConfigReader = new Class({
    timeBetweenReads: 30 * 24 * 60 * 60 * 1000, // a month
    languageListingURL: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623513185619/',
    initialize: function () {
    },
    checkForUpdates: function (languagename, callback) {
        var language = GM_getValue(UCPprefix + ".languageConfig." + languagename);
        if (!$chk(language)) {
		this.readLanguageConfigURL(languagename, function (retval) {
			if (!retval.success) {
				callback({ success: false, message: 'unable to read language list: ' + retval.message });
				return;
			}
			// sanity check
			if (!$chk(GM_getValue("UCP.languageConfig.list"))) {
				callback({ success: false, message: "internal error: no language list created; aborting" });
				return;
			}
			this.checkForUpdates(languagename, callback);
		}.bind(this));
		return; // the recursive call above will get to the block below
	}
	if ($chk(GM_getValue(UCPprefix + ".languageConfig." + languagename))) {
            var lastReadTime = GM_getValue(UCPprefix + ".languageConfig.lastReadTime." + languagename);
            var now = new Date().getTime();
            var elapsedTime = $chk(lastReadTime) ? now - lastReadTime : this.timeBetweenReads + 1;
            if (elapsedTime > this.timeBetweenReads) {
                GM_log("updating '" + languagename + "' definitions");
		GM_deleteValue("UCP.languageConfig.list");
		this.readLanguageConfigURL(languagename, callback);
	    } else {
		callback({ success: true });
	    }
        } else {
	    GM_log("updating " + languagename + " definitions");
            this.readLanguageConfigURL(languagename, callback);
        }
    },
    createLanguage: function (data) {
	var languagename = data.languagename;
	var legacyLabels = data.legacyLabels;
	var languageOverrides = data.languageOverrides;
	var callback = data.callback;

        if (!$chk(languagename)) {
            languagename = 'English';
        }
        if ($chk(ucpLanguages[languagename])) {
            callback({ success: true, language: ucpLanguages[languagename] });
	    return;
        }
        //GM_log("reading config for '" + languagename + "'");
        var storedConfig = null;
        if ($chk(GM_getValue(UCPprefix + ".languageConfig." + languagename))) {
		try {
	            storedConfig = GM_getObject(UCPprefix + ".languageConfig." + languagename);
		} catch(e) {
			GM_deleteValue(UCPprefix + ".languageConfig." + languagename);
		}
        }
        if (!$chk(storedConfig)) {
        //GM_log(languagename + " not found in storage, updating");
            this.readLanguageConfigURL(languagename, function(retval) {
		if (!retval.success) {
			callback({ success: false, message: 'unable to read language list: ' + retval.message });
			return;
		}
	    	// sanity check
		if (!$chk(GM_getValue(UCPprefix + ".languageConfig." + languagename))) {
			callback({ success: false, message: "internal error: no language list created; aborting" });
			return;
		}
		this.createLanguage(data);
	    }.bind(this));
	    return; // the recursive call above will get to the block below
        }
        // use the stored config, and read updates, in the background
        if ($chk(storedConfig)) {
        //GM_log("storing language " + languagename);
            GM_storeObject(UCPprefix + ".languageConfig." + languagename, storedConfig);
            // inject languagename
            storedConfig.language = languagename;
            var retval = new UCPLanguage(storedConfig);
            if ($chk(languageOverrides)) {
                retval.useLanguageOverrides(languageOverrides);
            }
            if ($chk(legacyLabels)) {
                retval.useLegacyLabels(legacyLabels);
            }
            ucpLanguages[languagename] = retval;
	    callback({ success: true, language: retval });
            return;
        }
        GM_log("result is invalid (storedConfig: " + storedConfig + ")");
        GM_deleteValue(UCPprefix + ".languageConfig." + languagename);
        // retry with English
        this.createLanguage({ languagename: 'English', legacyLabels: legacyLabels, languageOverrides: languageOverrides, callback: callback });
    },
    readLanguageConfigURL: function (languagename, callback) {
        //GM_log("reading language url for '" + languagename + "'");
    	var languageList = null;
        try {
            languageList = GM_getObject("UCP.languageConfig.list");
        } catch (e) { // parse error
            GM_deleteValue("UCP.languageConfig.list");
        }
        if (!$chk(languageList)) {
	    this.readLanguagelistURL(function(retval) {
		if (!retval.success) {
			callback(retval);
			return;
		}
		try {
        		languageList = GM_getObject("UCP.languageConfig.list");
		} catch (e) {
			callback({ success: false, message: 'error parsing language list: ' + e });
			return;
		}
		this.readLanguageConfigURL(languagename, callback); // recursive
		return;
	    }.bind(this));
        }
        var languageUrl = languageList[languagename].definitions;
        //GM_log("reading language definitions for '" + languagename + "': '" + languageUrl + "'");
        getThreadInfo({ threadId: languageUrl.match(/.*flickr.com\/groups\/[^\/]+\/discuss\/(\d+)/)[1],
            callback: function (retval) {
		var success = retval.success;
		var message = retval.message;
		if (!success) {
			callback(retval);
			return;
		}
                var languageConfiguration = retval.message
                                            .trim()
                                            .replace(/[\n\r]/g, '')
                                            .replace(/\"/g, '&quot;')
                                            .replace(/\\\'/g, '&#39;') // replace \' with its html number
                                            .replace(/\'{2}/g, "\"\"")
                                            // make the definitions strict JSON (use ' instead of " -> no &quot;)
                                            .replace(/\'([^\']+)\'/g, '\"$1\"') // replace 'xx' with "xx" for valid JSON
					    .replace(/&lt;/g, '<')
					    .replace(/&gt;/g, '>')
                                            // reset
                                            .replace(/&quot;/g, "\'");
                //GM_log("languageConfiguration: " + languageConfiguration);
                var onlineConfig;
                try {
                    onlineConfig = JSON.parse(languageConfiguration);
                } catch (e) {
                    GM_log("json error in language def: " + e);
                    GM_log("language: " + languageConfiguration);
                    try {
                        onlineConfig = eval ('(' + languageConfiguration + ')');
                    } catch (f) {
                        GM_log("error reading languageConfiguration: " + e);
                        GM_log("language configuration: " + languageConfiguration);
                        callback( { success: false, message: 'error reading languageConfiguration: ' + e } );
                        return;
                    }
                }
                if (languagename !== 'English') {
                    // reset defaults for non-defined parts, or languages
                    // TODO: document
                    this.createLanguage({ languagename: 'English', callback: function(retval) {
			var defaultLanguage = retval.language;
			this.checkForUpdates('English', function (retval) {
				if (!$chk(onlineConfig.titles)) {
					onlineConfig.titles = defaultLanguage.titles();
				    } else {
					for (var title in defaultLanguage.titles()) {
					    if (defaultLanguage.titles().hasOwnProperty(title)) {
						if (!$chk(onlineConfig.titles[title])) {
						//GM_log("title '" + title + "' not found, replacing with default");
						    onlineConfig.titles[title] = defaultLanguage.titles()[title];
						} else {
						    // reset the ' to &#39; conversion
						    onlineConfig.titles[title] = onlineConfig.titles[title]
							    .replace(/&#39;/g, "\\\'"); // html numbers don't work in html title attributes
						}
					    }
					}
				}
				if (!$chk(onlineConfig.labels)) {
					onlineConfig.labels = defaultLanguage.labels();
				} else {
					for (var label in defaultLanguage.labels()) {
					    if (defaultLanguage.labels().hasOwnProperty(label)) {
						if (!$chk(onlineConfig.labels[label])) {
						//GM_log("label '" + label + "' not found, replacing with default (" + defaultLanguage.labels()[label] + ")");
						    onlineConfig.labels[label] = defaultLanguage.labels()[label];
						}
					    }
					}
				}
				GM_storeObject(UCPprefix + ".languageConfig." + languagename, onlineConfig);
				GM_setValue(UCPprefix + ".languageConfig.lastReadTime." + languagename, new Date().getTime().toString()); 
				callback( { success: true } );
			});
		    }.bind(this) });
                } else {
			for (var title in onlineConfig.titles) {
			    if (onlineConfig.titles.hasOwnProperty(title)) {
				// reset the ' to &#39; conversion
				onlineConfig.titles[title] = onlineConfig.titles[title]
						    .replace(/&#39;/g, "'"); // html numbers don't work in html title attributes
			    }
			}
			GM_storeObject(UCPprefix + ".languageConfig." + languagename, onlineConfig);
			GM_setValue(UCPprefix + ".languageConfig.lastReadTime." + languagename, new Date().getTime().toString()); 
               		callback( { success: true } );
		}
            }.bind(this)
        });
    },
    readLanguagelistURL: function (callback) {
        //GM_log("reading language list");
        getThreadInfo({ threadId: this.languageListingURL.match(/.*flickr.com\/groups\/[^\/]+\/discuss\/(\d+)/)[1],
            callback: function(result) {
		if (!result.success) {
			callback(result);
			return;
		}
                var languageList = result.message
                                            .replace(/\'([^\']+)\'/g, '\"$1\"'); // replace 'xx' with "xx" for valid JSON
                this.languages = {};
                //GM_log("languageList: " + languageList);
                try {
                    this.languages = JSON.parse(languageList);
                } catch (e) {
                    GM_log("json error in language list: " + e);
                    GM_log("language list: " + languageList);
                    try {
                        this.languages = eval('(' + languageList + ')');
                    } catch (f) {
                        GM_log("error parsing languageList result: " + e);
                        GM_log("languageList: " + languageList);
                        callback( { success: false, message: 'error parsing languagelist result: ' + e } );
                        return;
                    }
                }
                GM_storeObject("UCP.languageConfig.list", this.languages);
                callback( { success: true } );
            }.bind(this)
        });
    },
    getLanguageList: function (callback) {
    	var list = null;
        try {
		list = GM_getObject("UCP.languageConfig.list");
		callback({ success: true, languageList: list });
		return;
        } catch (e) { // parse error
            GM_deleteValue("UCP.languageConfig.list");
        }
        this.readLanguagelistURL(function(retval) {
		if (!retval.success) {
			callback(retval);
			return;
		}
	    	try {
			list = GM_getObject("UCP.languageConfig.list");
			callback({ success: true, languageList: list });
		} catch (e) {
			callback({ success: false, message: 'error parsing language list: ' + e });
		}
	});
    }
});

function createTopicListingStatusDecorator (chlgstatus, ucpGroupPreferences, ucpLanguage) {
        switch (chlgstatus) {
        case "Filled":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().filled, 
                          ucpGroupPreferences.groupConfig().automaticVoteStart() ? ucpLanguage.labels().vote : ""
                        ], 
                title: ucpLanguage.titles().filled + 
                    (ucpGroupPreferences.groupConfig().automaticVoteStart() ? ucpLanguage.titles().automaticVoteStart : ""),
                color: ucpLanguage.labels().filledColor,
                addWarning: true
            };
        case "Finished":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().finished ],
                title: ucpLanguage.titles().finished,
                color: ucpLanguage.labels().finishedColor,
                addWarning: true
            };
        case "--VOTE--": // fall through
        case "VOTE":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().vote ],
                title: ucpLanguage.titles().vote,
                color: ucpLanguage.labels().voteColor,
                addWarning: true
            };
        case "Voted":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().voted ],
                title: ucpLanguage.titles().voted,
                color: ucpLanguage.labels().votedColor,
                addWarning: true
            };
        case "Voided": 
	case "Expired":
        case "Closed":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().closed ],
                title: ucpLanguage.titles().closed,
                color: ucpLanguage.labels().ignoreColor
            };
        case "Open":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().open ],
                title: ucpLanguage.titles().open,
                color: ucpLanguage.labels().openColor,
                addWarning: true
            };
        case "OK":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().waitingForEntries ],
                title: ucpLanguage.titles().open,
                color: ucpLanguage.labels().waitingForEntriesColor,
                addWarning: true
            };
        case "Excluded":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().excluded ],
                title: ucpLanguage.titles().excluded,
                color: ucpLanguage.labels().excludedColor,
                addWarning: true
            };
        case "ErrExclPlay":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().errExcl ],
                title: ucpLanguage.titles().errExclPlay,
                color: ucpLanguage.labels().errExclColor,
                addWarning: true
            };
        case "UPDATING":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().updating ],
                title: ucpLanguage.titles().updating,
                color: ""
            };
        case "ERRORLOADING":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().errorLoading ],
                title: ucpLanguage.titles().errorLoading,
                color: ucpLanguage.labels().errorLoadingColor,
                addWarning: true
            };
        case "ERRORPARSING":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().errorParsing ],
                title: ucpLanguage.titles().errorParsing,
                color: ucpLanguage.labels().errorParsingColor,
                addWarning: true
            };
        case "Player":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player ],
                title: ucpLanguage.titles().player,
                color: ucpLanguage.labels().playerColor,
                addWarning: true
            };
        case "PlayerVoted":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player, ucpLanguage.labels().voted ],
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().voted,
                color: ucpLanguage.labels().votedColor,
                addWarning: true
            };
        case "PlayerMustVote":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player, ucpLanguage.labels().vote ],
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().vote,
                color: ucpLanguage.labels().voteColor,
                addWarning: true
            };
        case "PlayerMayVote":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player ],
                title: ucpLanguage.titles().player,
                color: ucpLanguage.labels().playerColor,
                addWarning: true
            };
        case "PlayerFilled":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player, ucpLanguage.labels().filled ],
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().filled + 
                    (ucpGroupPreferences.groupConfig().automaticVoteStart() ? ucpLanguage.titles().automaticVoteStart : ""),
                color: ucpLanguage.labels().filledColor,
                addWarning: true
            };
        case "PlayerFinished":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player, ucpLanguage.labels().finished ],
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().finished,
                color: ucpLanguage.labels().finishedColor,
                addWarning: true
            };
        case "UNKNOWN":
        case "Unknown":
        case "---":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().ignore ],
                title: ucpLanguage.titles().ignore,
                color: ""
            };
        case "none":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().open + "?" ],
                title: ucpLanguage.titles().open,
                color: ucpLanguage.labels().openColor,
                addWarning: true
            };
        default:
            GM_log("no decorator for chlgstatus '" + chlgstatus + "'");
            return { name: "error" };
        }
}

function createChallengePageStatusDecorator (chlgstatus, ucpGroupPreferences, ucpLanguage) {
        switch (chlgstatus) {
        case "Filled":
            return { 
                name: chlgstatus,
                title: ucpLanguage.titles().filled + 
                    (ucpGroupPreferences.groupConfig().automaticVoteStart() ? ucpLanguage.titles().automaticVoteStart : "")
            };
        case "Finished":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().finished
            };
        case "--VOTE--": // fall through
        case "VOTE":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().vote
            };
        case "Voted":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().voted
            };
        case "Voided": 
	case "Expired":
        case "Closed":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().closed
            };
        case "Open":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().open
            };
        case "OK":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().open
            };
        case "Excluded":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().excluded
            };
        case "ErrExclPlay":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().errExclPlay
            };
        case "UPDATING":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().updating
            };
        case "ERRORLOADING":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().errorLoading
            };
        case "ERRORPARSING":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().errorParsing
            };
        case "Player":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player
            };
        case "PlayerVoted":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().voted
            };
        case "PlayerMustVote":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().vote
            };
        case "PlayerMayVote":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player
            };
        case "PlayerFilled":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().filled + 
                    (ucpGroupPreferences.groupConfig().automaticVoteStart() ? ucpLanguage.titles().automaticVoteStart : "")
            };
        case "PlayerFinished":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().finished
            };
        case "---":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().ignore
            };
        case "none":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().open
            };
        default:
            GM_log("don't know what to do with status '" + chlgstatus + "'");
            return { name: "error" };
        }
}

var global_group_preferences = null;

var ucpInsertionIdx = 0;

var UCPVoteComment = new Class({
    Implements: [Options],
    options: {
        ucpThread: null,
        poster: null,
        comments: [],
        node: null
    },
    initialize: function (options) {
	for (var key in options) {
		this.options[key] = options[key];
	}
    },
    toString: function () {
        return this.options.poster.username;
    },
    poster: function () {
        return this.options.poster;
    },
    node: function() {
        return this.options.node;
    },
    ucpThread: function () {
        return this.options.ucpThread;
    },
    printStatus: function () {
        if (this.options.ucpThread.options.needsStatusOnChallengePage) {
		var pageAnchor = $$('td.Who a[name=comment' + this.options.messageId + ']')[0].getParent('tr').getElements('small').getLast();
		if ($chk(pageAnchor) && $chk(pageAnchor.getParent('div.ucpdiv'))) {
		    pageAnchor = pageAnchor.getParent('div.ucpdiv');
		}
		if (!$chk(pageAnchor)) {
		    pageAnchor = this.options.node;
		}
		var message = "";
		if (this.options.ucpThread.challengeDefinition().scoreType() === "MEETANDGREET") {
		    message = "found a greeting from <b>" + this.poster().username;
		} else {
		    if ($chk(this.options.node) && $chk(this.options.node.getElement('img[alt*=UCPANG:bump]'))) {
			message = "found a bump message";

		    } else if ($chk(this.options.comments) && this.options.comments.length > 0) {
		    	this.options.comments.each(function (comment) {
		    		message = (message.length > 0 ? " - " : "") + comment.msg; // TODO: type: 'error'
		    	});
		    } else {
		    	message = "found a regular comment (no photo, no votes)";
		    }
		    message = message + " from " + this.poster().username;
		}
		new Element('small', {
		    html: "UCheckPlayNG: " + message + "<br/>"
		}).inject(new Element('div', { 'class': 'ucpdiv' }).inject(pageAnchor, 'after'));
	}
    }
});

var UCPThread = new Class({
    Implements: [Options],
    options: {
        groupConfig: undefined,
        chlgname: undefined,
        chlgAnchor: undefined,
        labelElement: undefined,
        feedbackElement: undefined,
        scoreAnchor: undefined,
        chlgstatus: "UPDATING",
        challengeDefinition: undefined,
        votingErrors: [],
	photoErrors: [],
        validVotingAsStored: true,
        excludedPlayers: [],
        url: undefined,
        topic: undefined,
        scoreSummary: undefined,
	cummulativeScore: undefined,
        needsStatusOnTopicListing: false,
        needsStatusOnChallengePage: false,
	reHorizontalVoteMatch: undefined
    },
    initialize: function (options) {
        this.setOptions(options);
        var reTopicMatch = /.*flickr.com\/groups\/[^\/.]*\/discuss\/([0-9]+)/;
        if (this.options.topic === undefined) {
            this.options.topic = reTopicMatch.exec(this.options.url)[1];
        }
    },
    toString: function () {
        return  [ "url:          " + this.url(),
                  "group:        " + this.groupname(),
                  "topic:        " + this.topic(),
                  "chlgname:     " + this.challengeName(),
                  "scroreSummary:" + this.scoreSummary(),
                  "chlgstatus:   " + this.challengeStatus(),
                  "validVoting:  " + this.validVoting(),
                  "votingErrors:  " + this.votingError(),
		  "photoErrors:  " + this.photoErrors()].join('\n');
    },
    // accessors
    groupConfig: function () {
        return this.options.groupConfig;
    },
    challengeName: function () {
        return this.options.chlgname;
    },
    setChallengeName: function (name) {
        this.options.chlgname = name;
    },
    challengeAnchor: function () {
        return this.options.chlgAnchor;
    },
    labelElement: function () {
        return this.options.labelElement;
    },
    setLabelElement: function (element) {
        this.options.labelElement = element;
    },
    scoreAnchor: function () {
        return this.options.scoreAnchor;
    },
    setScoreAnchor: function (element) {
        this.options.scoreAnchor = element;
    },
    feedbackElement: function (element) {
        return this.options.feedbackElement;
    },
    setFeedbackElement: function (element) {
        this.options.feedbackElement = element;
    },
    challengeStatus: function () {
        return this.options.chlgstatus;
    },
    setChallengeStatus: function (newstatus) {
        this.options.chlgstatus = newstatus;
    },
    challengeDefinition: function () {
        return this.options.challengeDefinition;
    },
    validVoting: function () {
        return (this.options.votingErrors.length === 0);
    },
    validVotingAsStored: function () {
        return this.options.validVotingAsStored;
    },
    votingError: function () {
        return this.options.votingErrors.join(" | ");
    },
    addVotingError: function (error) {
        if ($chk(error)) {
            this.options.votingErrors.include(error);
        }
    },
    photoErrors: function () {
           return this.options.photoErrors.join(" | ");
    },
    addPhotoError: function (error) {
           if ($chk(error)) {
                   this.options.photoErrors.include(error);
           }
    },
    addExcludedPlayer: function (user_name) {
        if ($chk(user_name)) {
            this.options.excludedPlayers.include(user_name);
        }
    },
    excludedPlayers: function () {
        return this.options.excludedPlayers;
    },
    isExcluded: function (user_name) {
        if ($chk(user_name)) {
            return this.options.excludedPlayers.contains(user_name);
        }
        return false;
    },
    findExcludesInText: function (html) {
        reExcludeMatch = this.groupConfig().reExcludeMatch();
    	//GM_log("checking for excludes in '" + html + "' with '" + reExcludeMatch + "'");
        var excludedMatch = reExcludeMatch.exec(html);
        if ($chk(excludedMatch)) {
            for (var exclMatchIdx = 1, exclMatchLen = excludedMatch.length; exclMatchIdx < exclMatchLen; ++exclMatchIdx) {
                var excludedPlayer = excludedMatch[exclMatchIdx];
                if (excludedPlayer) {
                    // special cases: admin, admins, administrators, I, me: ignore
                    this.addExcludedPlayer(excludedPlayer.replace(/&amp;/g, '&').replace(/<[^>]*>/g, '').replace(/&nbsp;/g, ' ').trim());
                }
            }
        }
    },
    url: function () {
        return this.options.url;
    },
    topic: function () {
        return this.options.topic;
    },
    scoreSummary: function () {
        return this.options.scoreSummary;
    },
    setScoreSummary: function (summ) {
        this.options.scoreSummary = summ;
    },
    cummulativeScore: function () {
    	return this.options.cummulativeScore;
    },
    setCummulativeScore: function (vote) {
    	this.options.cummulativeScore = vote;
    },
    groupname: function () {
        return this.groupConfig().groupname();
    },
    updateStatus: function (value) {
    	//GM_log("current status: " + this.options.chlgstatus + ", value: " + value);
        if (this.challengeStatus() === "UPDATING") {
            this.setChallengeStatus("none");
        }
        if (value === "closed") {
            this.setChallengeStatus("Closed");
            return;
        }
        if (value === "voided") {
            this.setChallengeStatus("Voided");
            return;
        }
	if (value === "expired") {
	    this.setChallengeStatus("Expired");
	    return;
	}
        if ((this.challengeStatus() === "none") && (value === "open")) {
            this.setChallengeStatus("Open");
            return;
        }
        // set for photoposter (first for loop)
        if (value === "Excluded") {
            this.setChallengeStatus(this.filled() ? "Filled" : "Excluded");
            return;
        }
        if ((this.challengeStatus() === "none") && (value === "photoposter")) {
            this.setChallengeStatus("Player");
            return;
        }
        if ((this.challengeStatus() === "Excluded") && (value === "photoposter")) {
            this.setChallengeStatus("ErrExclPlay");
            return;
        }
        if ((this.challengeStatus() === "none") && (value === "filled")) {
            this.setChallengeStatus("Filled");
            return;
        }
        // set for voter (second for loop)
        if ((this.challengeStatus() === "none") && (value === "voter")) {
            this.setChallengeStatus("Voted");
            return;
        }
        if ((this.challengeStatus() === "Excluded") && (value === "voter")) {
            this.setChallengeStatus("Voted");
            return;
        }
        if ((this.challengeStatus() === "ErrExclPlay") && (value === "voter")) {
            this.setChallengeStatus("ErrExclPlay");
            return;
        }
        if ((this.challengeStatus() === "Player") && (value === "voter")) {
            this.setChallengeStatus("PlayerVoted");
            return;
        }
        if ((this.challengeStatus() === "Voted") && (value === "voter")) {
            this.setChallengeStatus("Voted"); //catch a comment and a vote from same player
            return;
        }
        if ((this.challengeStatus() === "Voted") && (value === "photoposter")) {
            this.setChallengeStatus("PlayerVoted");
            return;
        }
        if ((this.challengeStatus() === "Player") && (value === "mayvote")) {
            this.setChallengeStatus("PlayerMayVote");
            return;
        }
        if ((this.challengeStatus() === "Player") && (value === "mustvote")) {
            this.setChallengeStatus("PlayerMustVote");
            return;
        }
        if (this.challengeStatus().match(/^Player/) && (value === "voter")) {
            this.setChallengeStatus("PlayerVoted");
            return;
        }
        if (this.challengeStatus().match(/^Player/) && (value === "finished")) {
            this.setChallengeStatus("PlayerFinished");
            return;
        }
        if (this.challengeStatus().match("Finished") && (value === "photoposter")) {
            this.setChallengeStatus("PlayerFinished");
            return;
        }
        if (this.challengeStatus() === "Player") {
            this.setChallengeStatus("Player"); // don't overwrite in case of 'maynotvote'
            return;
        }
        if (value === "finished") {
            this.setChallengeStatus("Finished");
            return;
        }
        if ((this.challengeStatus() === "none") && (value === "Unknown")) {
            this.setChallengeStatus("Unknown");
            return;
        }
        // this.setChallengeStatus(""); keep the status as it is! => [Finished] => RE-VOTE => voter => error!
    },
    printStatus: function (ucpGroupPreferences, ucp_language, newchlgstatus) {
        if (!newchlgstatus || newchlgstatus.length === 0) {
            newchlgstatus = this.challengeStatus();
        }
        var statusData;
        if (this.options.needsStatusOnTopicListing) {
            statusData = createTopicListingStatusDecorator(newchlgstatus, ucpGroupPreferences, ucp_language);
        } else if (this.options.needsStatusOnChallengePage) {
            statusData = createChallengePageStatusDecorator(newchlgstatus, ucpGroupPreferences, ucp_language);
        } else {
            return;
        }
        this.decorateThread(statusData);
        // TODO: part of UCPListing.printStatus
        this.groupConfig().checkPlayerEntries({
                entries: playernumber
        });
    },
    decorateThread: function (statusData) {
        if (this.options.needsStatusOnTopicListing) {
            if ($chk(this.scoreAnchor()) && $chk(this.scoreSummary()) && 
                (statusData.name === "Finished"    || statusData.name === "Voted" || 
                 statusData.name === "PlayerVoted" || statusData.name === "PlayerFinished" ||
                 statusData.name === "Closed")) {
                this.scoreAnchor().title = "UCheckPlay score summary: " + this.scoreSummary();
            }

            if (this.labelElement()) {
                this.labelElement().empty();
                this.labelElement().style.textDecoration = 'none';
                var myColor = statusData.color;
                var span = null;
                statusData.labels.each(function (label) {
                    this.labelElement().adopt(span = new Element('span', {
                        html: label,
                        styles: {
                            color: myColor
                        },
                        title: statusData.title
                    }));
                }, this);
                if (statusData.addWarning && !this.validVoting()) {
                    span.set('html', span.get('html') + '(!)');
                    span.set('title', span.get('title') + ' (!! ' + this.votingError() + ')');
                }
    	    	if (this.usesTimeConstraints()) {
		    if (statusData.name == "OK" || statusData.name == "Open" || statusData.name == "--VOTE--" || statusData.name.match("Player") || statusData.name.match("Voted") ) {
			var flickrClock = calculateFlickrClock();
			var deadline = $chk(this.voteDeadline()) ? this.voteDeadline() : this.postDeadline();
			if ($chk(flickrClock) && $chk(deadline)) {
			    var timeleft = deadline - flickrClock.getTime();
			    if (timeleft > 0) {
				var statusElement = $('UCPNG.' + this.challengeAnchor());
				var timeleftElement = $('UCPNG.timeleft.' + this.challengeAnchor());
				if ($chk(statusElement) && !$chk(timeleftElement)) {
			    		statusElement = statusElement.getParent('div.UCP_status_column');
					var minutesFactor = 1000 * 60;
					var hoursFactor = minutesFactor * 60;
					var daysFactor = hoursFactor * 24;

					var daysLeft = Math.floor(timeleft / daysFactor);
					if (daysLeft >= 1) {
						var timeleftElement = new Element('span', { 
							id: 'UCPNG.timeleft.' + this.challengeAnchor(), 
							html: daysLeft + "&nbsp;day" + (daysLeft >= 2 ? "s" : "")
						}).inject(statusElement);
					} else {
						timeleftElement = new Element('span', {
							id: 'UCPNG.timeleft.' + this.challengeAnchor(),
							html: 'final&nbsp;day!'
						}).inject(statusElement);
					}
					var hoursLeft = Math.floor((timeleft - daysLeft * daysFactor) / hoursFactor);
					var minutesLeft = Math.floor((timeleft - daysLeft * daysFactor - hoursLeft * hoursFactor) / minutesFactor);
					timeleftElement.set('title', daysLeft + 'd' + hoursLeft + 'h' + minutesLeft + 'm');
				}
			    }
	    		}
		    }
		}
            }
            if (this.challengeAnchor()) {
                this.challengeAnchor().set('title', this.options.title); // check: this.options.title does not exist?
            }
        } else if (this.options.needsStatusOnChallengePage) {
            var showSummary = $chk(this.scoreAnchor()) && $chk(this.scoreSummary()) && 
                (statusData.name === "Finished"    || statusData.name === "Voted" || 
                 statusData.name === "PlayerVoted" || statusData.name === "PlayerFinished" ||
                 statusData.name === "Closed");
            if (showSummary) {
                this.scoreAnchor().title = "UCheckPlayNG score summary: " + this.scoreSummary();
            }

            if (this.feedbackElement()) {
                this.feedbackElement().empty();
                if (statusData.title) {
                    this.feedbackElement().set('html', 'UCheckPlayNG: ' + statusData.title + '<br/>');
                }
                if ($chk(this.scoreSummary())) {
                    new Element('small', {
                        html: "UCheckPlayNG: " + this.scoreSummary() + "<br/>"
                    }).inject(this.feedbackElement(), 'after');
                }
            }
        }
    },
    printExcludes: function () {
        if (!this.options.needsStatusOnChallengePage) {
            return;
        }
        try {
            if ($chk(this.options.excludedPlayers) && this.options.excludedPlayers.length > 0) {
                var pageAnchor = $(document).getElement('td.Said').getElements('small').getLast();
                if ($chk(pageAnchor) && $chk(pageAnchor.getParent('div.ucpdiv'))) {
                    pageAnchor = pageAnchor.getParent('div.ucpdiv');
                }
                if (!$chk(pageAnchor)) {
                    var players = this.excludedPlayers().join('</b>, and <b>');
			reportStatus("UCheckPlayNG: found excludes for <b>" + players + "</b>");
                } else {
			this.excludedPlayers().each(function (excludedPlayer) {
				new Element('small', {
				    html: "UCheckPlayNG: found exclude for <b>" + excludedPlayer + "</b><br/>"
				}
			    ).inject(pageAnchor, 'after');
			});
		}
            }
        } catch (e) {
            GM_log("error: " + e);
        }
    },
    checkStatus: function () {
        if (this.challengeName().match(this.groupConfig().states().closed)) {
            this.updateStatus("closed");
        }
        if (this.challengeName().match(this.groupConfig().states().voided)) {
            this.updateStatus("voided");
        }
	if (this.challengeName().match(this.groupConfig().states().expired)) {
	    this.updateStatus("expired");
	}
        if ((this.challengeStatus() === "none") && this instanceof UCPChallengeThread) {
		if (this.filled()) {
			this.updateStatus("filled");
        	} else if (this.open()) {
            		this.setChallengeStatus("Open");
       	 	} else if (this.waitingForEntries()) {
            		this.setChallengeStatus("OK");
        	}
	}
        if (  (this.challengeStatus() === "none" || this.challengeStatus() === "Excluded") 
            && $chk(this.challengeName().match(this.groupConfig().states().vote))) {
            this.setChallengeStatus("--VOTE--");
        }
        if ((this.challengeStatus() === "Player") && 
            (this.filled() || this.challengeName().match(this.groupConfig().states().vote))) {
            this.updateStatus(this.challengeDefinition().playerVoting());
        }
	if (this.challengeStatus() === "Excluded" && this.filled()) {
		this.setChallengeStatus("Filled");
	}
        if (this.challengeStatus() === "PlayerMayVote" || this.challengeStatus() === "Player") {
            if (this.filled()) {
                this.setChallengeStatus("PlayerFilled");
            }
            return;
        }
    },
    resetStatus: function () {
        this.setChallengeStatus("none");
        this.options.votingErrors = [];
	this.options.photoErrors = [];
    },
    loadthread: function (processDiscussionTopicCallback) {
	var ucpThread = this;
            var apiData = {
                api_key: GM_getPrivateKey(),
                auth_hash: GM_getAuthHash(),
                auth_token: GM_getAuthToken(),
                format: 'json',
                method: 'flickr.groups.discuss.replies.getList',
                nojsoncallback: 1,
                topic_id: this.topic(),
		page: 1,
		per_page: 500
            };
            new Request({
                url: "http://api.flickr.com/",
                onSuccess: function (responseText, responseXML) {
                    var result;
                    try {
                        result = JSON.parse(responseText);
                    } catch (e) {
			GM_log("error parsing JSON - evaluating");
			try {
                        	result = eval('(' + responseText + ')');
			} catch (f) {
				processDiscussionTopicCallback({ success: false, message: e, ucpThread: ucpThread });
				return;
			}
                    }
                    if (result.stat === 'fail') {
                        processDiscussionTopicCallback({ success: false, message: result.code + " - " + result.message, ucpThread: ucpThread });
			return;
                    }
		    var discussionTopic = result.replies;
		    processDiscussionTopicCallback({ success: true, discussionTopic: discussionTopic, ucpThread: ucpThread });
                },
		onFailure: function (response) {
			GM_log("error: " + response.statusText);
			processDiscussionTopicCallback({ success: false, message: response.statusText });
		}
            }).get("/services/rest/", apiData);
    },
    collectVotes: function (challengeAnnouncement, challengeEntries, callback) {
        var group_config = this.groupConfig();
        var challengeConfig = this.challengeDefinition();
        var topic = this.topic();
        var voteArray = [];
        var commentArray = [];
        var photoArray = [];
   
        if (this.groupConfig().allowsPhotoInAnnouncement()) {
            photoArray.combine(this.findPhotos({ entry: challengeAnnouncement, all: false }));
        }
        challengeEntries.each(function (challengeEntry, i) {// try to find the number of photos posted; helps with constructing the vote regexp
            if (i === 1 && group_config.skipFirstReply()) { 
                return;
            }
            //GM_log("["+i+"]: "+challengeEntry.innerHTML);
            // admins have an image before there name, surrounded with an 'a' tag without 'href' attribute :> skip
            var poster = { username: challengeEntry.authorname, userid: challengeEntry.author, admin: challengeEntry.role == "admin" || challengeEntry.role == "moderator" }; 

            var photosInNode = this.findPhotos({ entry: challengeEntry, all: false, poster: poster });

            if (photosInNode.length > 0) {
                photoArray.combine(photosInNode);
		// record for the next loop
		challengeEntry.photos = photosInNode;
		challengeEntry.poster = poster;
	    }
	},this);
        this.photosArray = photoArray;
	challengeEntries.each(function (challengeEntry, i) {
            if (i === 1 && group_config.skipFirstReply()) { 
                return;
            }
	    if ($chk(challengeEntry.photos)) {
		return;
	    }
            // This should be a vote or user comment
            var poster = { username: challengeEntry.authorname, userid: challengeEntry.author, admin: challengeEntry.role == "admin" || challengeEntry.role == "moderator" }; 

            if (i === 2 && group_config.skipFirstTwoRepliesForVotes()) {
                    return;
            }
	    
                var usercommentClone = new Element('div', { html: challengeEntry.message._content });
                // only consider votes that are after the last photo (only approximate)
                // if the next comment has a photo, skip!
                if (voteArray.length === 0 && challengeEntries.length > i) {
                    var nextComment = challengeEntries[i + 1];
                    if (nextComment && this.findPhotos({ entry: nextComment, all: false }).length > 0) {
                        commentArray.include(new UCPVoteComment({
                            node: usercommentClone,
			    messageId: challengeEntry.id,
                            poster: poster,
                            ucpThread: this
                        }));
                        return;
                    }
                }
                // first remove striked votes, image icons, banners
                var strikesAndStuff = usercommentClone.getElements("s, del, strike, img, a");
                strikesAndStuff.each(function (strike) {
                    strike.dispose();
                });
                var replytxt = usercommentClone.innerHTML;
        
                var exampleVote = false;
                if (replytxt.match(/sample vote|example|ejemplo/i) && !document.location.href.match(/matchpoint/)) {
                    // in MatchPoint commenting is encouraged, often leading to "nice example of", or similar
                    exampleVote = true;
                }

                switch (challengeConfig.scoreType()) {
                case "VERTICAL":
                    var vote = parseVerticalVote({
                                   replytxt: replytxt, 
                                   challengeEntry: usercommentClone,
				   messageId: challengeEntry.id,
                                   ucpThread: this,
                                   poster: poster,
                                   exampleVote: exampleVote
                         });
                    if (vote) {
                        if (vote instanceof UCPVoteComment) {
                            commentArray.include(vote);
                        } else {
                            voteArray.include(vote);
                        }
                    }
                    break;
                
                case "VERTICAL-WEIGHTED":
                    var vote = parseVerticalWeightedVote({
                                    replytxt: replytxt, 
                                    challengeEntry: usercommentClone,
				    messageId: challengeEntry.id,
                                    ucpThread: this,
                                    poster: poster,
                                    exampleVote: exampleVote
                                });
                    if (vote) {
                        if (vote instanceof UCPVoteComment) {
                            commentArray.include(vote);
                        } else {
                            voteArray.include(vote);
                        }
                    }
                    break;

                case "RATE-PHOTO":
                    var vote = parseRatePhotoVote({
                                    replytxt: replytxt, 
                                    challengeEntry: usercommentClone,
				    messageId: challengeEntry.id,
                                    ucpThread: this,
                                    poster: poster,
                                    exampleVote: exampleVote
                                });
                    if (vote) {
                        if (vote instanceof UCPVoteComment) {
                            commentArray.include(vote);
                        } else {
                            voteArray.include(vote);
                        }
                    }
                    break;
               
                case "HORIZONTAL":
                    var vote = parseHorizontalVote({
                                    replytxt: replytxt, 
                                    challengeEntry: usercommentClone,
				    messageId: challengeEntry.id,
                                    ucpThread: this,
                                    poster: poster,
                                    exampleVote: exampleVote
                                });
                    if (vote) {
                        if (vote instanceof UCPVoteComment) {
                            commentArray.include(vote);
                        } else {
                            voteArray.include(vote);
                        }
                    }
                    break;

                case "MEETANDGREET":
                    commentArray.include(new UCPVoteComment({
                        poster: poster, 
			messageId: challengeEntry.id,
                        node: usercommentClone,
                        ucpThread: this
                    }));
                    break;

                default:
                //case "PIC-*":
                    if (/PIC-/.test(challengeConfig.scoreType())) {
                        // PIC-V-n: pic n photos, score vertically
                        // PIC-H-n: pic n photos, score horizontally
                        // PIC-P-x: give a point to the player
                        var picXmatch = /PIC-([HVP])-(\d+)/.exec(challengeConfig.scoreType());
                        var picX = undefined;
                        var picOrientation = undefined;
                        if (picXmatch && picXmatch.length > 2) {
                            picX = parseInt(picXmatch[2], 10);
                        }
                        if (picXmatch && picXmatch.length > 1) {
                            picOrientation = picXmatch[1];
                        }
                        if (picOrientation === 'H') {
                            var createdPicHVote = parseHorizontalVote({ 
                                                    exampleVote: exampleVote,
                                                    ucpThread: this, 
                                                    poster: poster, 
                                                    challengeEntry: usercommentClone,
						    messageId: challengeEntry.id, 
                                                    replytxt: replytxt
                                                });
                            if (createdPicHVote instanceof UCPVoteComment) {
                                commentArray.include(createdPicHVote);
                            } else {
                                voteArray.include(createdPicHVote);
                            }
                        } else if (picOrientation === 'V') {
                            // PIC-V
                            var createdPicVVote = parsePICVerticalVote({ 
                                                    exampleVote: exampleVote,
                                                    ucpThread: this, 
                                                    poster: poster, 
                                                    challengeEntry: usercommentClone,
						    messageId: challengeEntry.id, 
                                                    replytxt: replytxt,
                                                    picX: picX
                                                });
                            if (createdPicVVote instanceof UCPVoteComment) {
                                commentArray.include(createdPicVVote);
                            } else {
                                voteArray.include(createdPicVVote);
                            }
                        } else if (picOrientation === 'P') {
                            var createdPicPVote = parsePICPlayerVote({
                                                    exampleVote: exampleVote,
                                                    ucpThread: this,
                                                    poster: poster,
                                                    challengeEntry: usercommentClone,
						    messageId: challengeEntry.id,
                                                    replytxt: replytxt,
                                                    photoArray: photoArray,
						    picX: picX
                                                });
                            if (createdPicPVote instanceof UCPVoteComment) {
                                commentArray.include(createdPicPVote);
                            } else {
                                voteArray.include(createdPicPVote);
                            }
                        }
                    }
                }
        }, this); // each entry

        this.votesArray = voteArray;
        this.commentsArray = commentArray;
	callback();
    },
    findPhotos: function(data) {
    	var entry = data.entry;
	var all = data.all;
	var poster = data.poster;
	var _debug = data.debug;

        var photoArray = [];
        if (this.challengeDefinition().scoreType() === "MEETANDGREET" && !all) { // don't bother
            return photoArray;
        }
	if (!entry.message._content.test('<img')) {
		return photoArray;
	}
	if (!$chk(poster)) {
            poster = { username: entry.authorname, userid: entry.author, admin: entry.role == "admin" || entry.role == "moderator" }; 
	}
	var entryDiv = new Element('div', {
                    html: entry.message._content
                });
        var photos = entryDiv.getElements("img").filter(ucpCheckPhoto, this.challengeDefinition());
        var potentialCompetitor = null;
        photos.each(function(photoNode) {
            if (potentialCompetitor !== null && !all) {
                return;
            }
	    var photoNumber = undefined;
	    if (global_group_preferences.checkPhotoNumbering()) {
	    	var numberP = photoNode.getParent('div');
		    numberP.get('text').split('\n').some( function (line) {
    			try {
				photoNumber = line.trim().replace(/^(&nbsp;|&emsp;|\r)+/, '').match(/^[#~]?\s*(\d+)/)[1];
				return true;
		    	} catch (e) {
		    		// ignore: most lines won't match
	    		}
			return false;
	    	    });
	    }
            potentialCompetitor = ucpCreateCompetitor({
		messageId: entry.id,
	    	node: photoNode, 
		poster: poster,
		number: photoNumber,
		ucpThread: this,
		debug: _debug
	    });
            if (potentialCompetitor) {
                photoArray.push(potentialCompetitor);
            }
        }.bind(this));
        return photoArray;
    },
    isClosed: function (bodyElement) {
	if (!$chk(bodyElement)) {
		bodyElement = $$('body')[0];
	}
	var closed = $chk(bodyElement.getElement('p.Focus * a[href*=unlock]'));
	if (!closed) {
		return { closed: false };
	}
	var closer = bodyElement.getElement('p.Focus');
	return {
		closed: true,
		closer: closer
	};
    },
    isUnanimous: function () {
    	switch (this.challengeDefinition().scoreType()) {
		case 'HORIZONTAL':
		case 'VERTICAL':
			return this.votes().length == 0 ? false : this.votes()[this.votes().length - 1].isUnanimous();
		case 'PIC-V-1':
			this.votes().each( function (vote) {
				GM_log("DEBUG: vote=" + vote);
			});
			return false;
		case 'PIC-P-1':
			// TODO
		default: 
			return false; // PIC-P-n, PIC-V-n, PIC-H-n with n > 1 are never anonimous
	}
					// same for RATE-PHOTO, VERTICAL-WEIGHTED
    }
});

var UCPNonChallengeThread = new Class({
    Extends: UCPThread,
    initialize: function (options) {
        this.parent(options);
    },
    printStatus: function (group_preferences, ucp_language, newchlgstatus) {
        var anchortitle = ucp_language.titles()[this.getLabelPrefix()];
        if (this.labelElement()) {
            this.labelElement().set('html', ucp_language.labels()[this.getLabelPrefix()]);
            this.labelElement().set ('style', 
                'color: ' + ucp_language.labels()[this.getLabelPrefix() + "Color"] + '; text-decoration: none');
            this.labelElement().title = anchortitle;
            this.challengeAnchor().title = anchortitle;
        }
    },
    getLabelPrefix: function () {},

    photos: function () {
        return [];
    },
    votes: function () {
        return [];
    },
    comments: function () {
        return [];
    },
    hasError: function () {
    	return false;
    },
    hasWarning: function () {
    	return false;
    }
});

var UCPChatThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Chat");
    },
    getLabelPrefix: function () {
        return "chat";
    }
});

var UCPShowroomThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Showroom");
    },
    getLabelPrefix: function () {
        return "showroom";
    }
});

var UCPGameThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Game");
    },
    getLabelPrefix: function () {
        return "game";
    }
});

var UCPMeetAndGreetThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("MeetAndGreet");
    },
    getLabelPrefix: function () {
        return "meetAndGreet";
    }
});

var UCPInformationThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Info");
    },
    getLabelPrefix: function () {
        return "info";
    }
});

/*var UCPVoidedThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Voided");
    },
    getLabelPrefix: function () {
        return "ignore";
    }
});*/

var UCPUnknownThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Unknown");
    },
    getLabelPrefix: function () {
        return  "ignore";
    }
});

var UCPChallengeThread = new Class({
    Extends: UCPThread,
    initialize: function(options) {
        this.parent(options);
    },
    filled: function () {
        if (this.challengeDefinition().neededPhotos() <= 0) {
	    if (this.challengeName().match(this.groupConfig().states().open) || this.challengeName().match(this.groupConfig().states().waitingForEntries)) {
    	    	if ($chk(this.postDeadline()) || this.usesTimeConstraints()) {
		    var flickrClock = calculateFlickrClock();
		    if ($chk(flickrClock)) {
		        if ($chk(this.postDeadline())) {
				var postDeadlineMillis = this.postDeadline();
			} else {
				var filledRegExp = this.groupConfig().states().filledRegExp;
				if (!$chk(filledRegExp) || !$chk(filledRegExp.replacement_function)) {
					GM_log("ERROR: missing 'filledFunction'");
					return false;
				}
				postDeadlineMillis = this.challengeName().replace(new RegExp(filledRegExp.expression, filledRegExp.flags),
									new Function(filledRegExp.replacement_function));
				this.setPostDeadline(postDeadlineMillis);
			}
			var timeleft = postDeadlineMillis - flickrClock.getTime();
			if (timeleft <= 0) {
				return true;
			}
			return false;
		    }
		    return false;
		}
	    }
            return false;
        }
        if (this.photos().length >= this.challengeDefinition().neededPhotos() &&
                (this.challengeName().match(this.groupConfig().states().open) || 
                 this.challengeName().match(this.groupConfig().states().waitingForEntries))) {
            return true;
        }
        return false;
    },
    open: function () {
        //GM_log("checking name against open states: " + this.chlgname + " - " + states.open + " - " + states.waitingForEntries);
        if (this.photos().length === 0 && 
                (this.challengeName().match(this.groupConfig().states().open) || 
                 this.challengeName().match(this.groupConfig().states().waitingForEntries))) {
            return true;
        }
        return false;
    },
    waitingForEntries: function () {
        if (this.photos().length == 0) {
            return false;
        }
        if (this.challengeDefinition().neededPhotos() < 0 &&
                (this.challengeName().match(this.groupConfig().states().open) || 
                 this.challengeName().match(this.groupConfig().states().waitingForEntries))) {
            return true;
        }
        if (this.challengeDefinition().neededPhotos() < 0) {
            return false;
        }
        return (this.photos().length < this.challengeDefinition().neededPhotos()
                && (this.challengeName().match(this.groupConfig().states().waitingForEntries) ||
                    this.challengeName().match(this.groupConfig().states().open)));
    },
    finished: function(vote) {
	/*if (!$chk(vote)) { // vote is normally the cummulative score
                if (this.votes().length == 0) {
                        return false;
                }
                vote = this.votes()[this.votes().length - 1];
        }*/ // check: copied from admin script - needed here?
        // special case: PIC-P requires a diff of 2 points
        if (this.challengeDefinition().scoreType().match(/PIC-P-/) && this.challengeDefinition().neededScore() > 0) {
            if (vote.topDiff() >= 2 &&
                this.challengeDefinition().scoresAdded() && 
                vote.maxVote >= this.challengeDefinition().neededScore()) {
                return true;
            }
            return false;
        }
        if (this.challengeDefinition().neededScore() === -1) {
	    if (this.challengeName().match(this.groupConfig().states().vote)) {
    	    	if (this.usesTimeConstraints() || $chk(this.voteDeadline()) ) {
	    	    var flickrClock = calculateFlickrClock();
		    if ($chk(flickrClock)) {
		        if ($chk(this.voteDeadline())) {
		    	    var voteDeadlineMillis = this.voteDeadline();
		        } else {
			    var finishedRegExp = this.groupConfig().states().finishedRegExp;
			    if (!$chk(finishedRegExp) || !$chk(finishedRegExp.replacement_function)) {
				GM_log("ERROR: missing 'finishedFunction'");
				return false;
			    }
			    voteDeadlineMillis = this.challengeName().replace(new RegExp(finishedRegExp.expression, finishedRegExp.flags),
									new Function(finishedRegExp.replacement_function));
			    this.setVoteDeadline(voteDeadlineMillis);
		        }
		        var timeleft = voteDeadlineMillis - flickrClock.getTime();
		        if (timeleft <= 0) {
			    return true;
		        }
		        return false;
		    }
		    return false;
	        }
                return false;
	    }
	    return false;
        }
        if (this.challengeDefinition().scoresAdded() && 
            vote.maxVote >= this.challengeDefinition().neededScore()) {
            return true;
        }
        if (!this.challengeDefinition().scoresAdded() && vote.isUnanimous()) {
            return true;
        }
        return false;
    },
    resetStatus: function() {
        this.parent();
        this.checkStatus();
    },
    hasError: function () {
        if (this.votes().some(function(vote) {
                        return vote.hasError();
                })) {
                return true;
        }
        if (this.photos().some(function(photo) {
                        return photo.hasError();
                })) {
                return true;
        }
        return false;
    },
    hasWarning: function () {
        if (this.votes().some(function(vote) {
                        return vote.hasWarning();
                })) {
                return true;
        }
        if (this.photos().some(function(photo) {
                        return photo.hasWarning();
                })) {
                return true;
        }
        return false;
    },
    photos: function () {
        if (this.photosArray == undefined) {
                return [];
        }
        return this.photosArray;
    },  
    votes: function () {
        if (this.votesArray == undefined) {
                return [];
        }
        return this.votesArray;
    },  
    comments: function () {
        if (this.commentsArray == undefined) {
                return [];
        }
        return this.commentsArray;
    },
    error: function () {
        var retval = "";
        this.photos().each(function (photo) {
                if (photo.hasError()) {
                    if (retval.length > 0) retval += " - ";
                    retval += photo.poster().username + ": " + photo.error();
                }
        });
        this.votes().each(function (vote) {
                if (vote.hasError()) {
                    if (retval.length > 0) retval += " - ";
                    retval += vote.poster().username + ": " + vote.error();
                }
        });
        return retval;
    },
    warning: function () {
        var retval = "";
        this.photos().each(function (photo) {
                if (photo.hasWarning()) {
                    if (retval.length > 0) retval += " - ";
                    retval += photo.poster().username + ": " + photo.warning();
                }
        });
        this.votes().each(function (vote) {
                if (vote.hasWarning()) {
                    if (retval.length > 0) retval += " - ";
                    retval += vote.poster().username + ": " + vote.warning();
                }
        });
        return retval;
    },
    usesTimeConstraints: function () {
    	if ($chk(this.challengeDefinition().usesTimeConstraints())) {
		return this.challengeDefinition().usesTimeConstraints();
	}
	return this.groupConfig().usesTimeConstraints();
    },
    setUsesTimeConstraints: function (u) {
    	this.challengeDefinition().setUsesTimeConstraints(u);
    },
    postDeadline: function () {
    	return this.challengeDefinition().postDeadline();
    },
    setPostDeadline: function (millis) {
    	this.challengeDefinition().setPostDeadline(millis);
    },
    voteDeadline: function () {
    	return this.challengeDefinition().voteDeadline();
    },
    setVoteDeadline: function (millis) {
    	this.challengeDefinition().setVoteDeadline(millis);
    },
    reHorizontalVoteMatch: function() {
	    if (!this.options.reHorizontalVoteMatch) {
		// xX: when a photo is removed once voting has started, it may be replaced with x: 1-2-x-0
		// when split with a character, it can be double digits
		var reVoteMatchSplit  = "(?:\\b|\\s)(\\d+|[xX]{1})\\s*[^xX\\d]{1,2}\\s*(\\d+|[xX]{1})"; // at least 2
		// when stuck to each other, it should be single digits
		var reVoteMatchJoined = "(?:\\b|\\s)(\\d{1}|[xX]{1})(\\d{1}|[xX]{1})"; // at least 2
			
		// limit the seperation characters to non-alphabet characters:
//GM_log("posted photos: " + this.photos().length + " - needed photos: " + this.challengeDefinition().neededPhotos());
		if (this.photos().length == this.challengeDefinition().neededPhotos()) {
			if (this.photos().length > 2) { // we already have a regexp for 2
				for (var p = 0; p < this.photos().length - 2; ++p) {
				    reVoteMatchSplit  = reVoteMatchSplit  + "\\s*(?:[^xX\\d]{1,2})\\s*(\\d+|[xX]{1})";
				    reVoteMatchJoined = reVoteMatchJoined + "(\\d{1}|[xX]{1})";
				}
			}
		} else {
			for (var p = 1; p < 10; ++p) {
			    reVoteMatchSplit  = reVoteMatchSplit  + "\\s*(?:[^xX\\d]{1,2})?\\s*(\\d+|[xX]{1})?";
			    reVoteMatchJoined = reVoteMatchJoined + "(\\d{1}|[xX]{1})?";
			}
		}
		reVoteMatchSplit  = reVoteMatchSplit  + "(?:\\s|\\b)";
		reVoteMatchJoined = reVoteMatchJoined + "(?:\\s|\\b)";
		this.options.reHorizontalVoteMatch = new RegExp(reVoteMatchSplit + "|" + reVoteMatchJoined, "ig");
//GM_log("using regexp '" + this.options.reHorizontalVoteMatch + "'");
	    }
	    return this.options.reHorizontalVoteMatch;
    }

});

function ucpCreateChallengeThread(options) {



    var cc_group_config = options.groupConfig;
    var chlgname = options.chlgname;
    var challengeDefinition = cc_group_config.extractChallengeDefinition(chlgname);
    options.challengeDefinition = challengeDefinition;
    switch (challengeDefinition.scoreType()) {
    case "CHAT":
        return new UCPChatThread(options);
    case "SHOWROOM":
        return new UCPShowroomThread(options);
    case "GAME":
        return new UCPGameThread(options);
    case "MEETANDGREET":
        return new UCPMeetAndGreetThread(options);
    case "INFO":
        return new UCPInformationThread(options);
    case "UNKNOWN":
        return new UCPUnknownThread(options);
    default:
        return new UCPChallengeThread(options);
    }
}

var UCPGroupPreferences = new Class({
/* should be completely different from other UCP script */
    Implements: [Options],
    options: {
        groupConfig: null, // mandatory
        // UCheckPlay preferences
        ucpStyle: false, // defaults to flickrStyle
        language: 'English',
        useLegacyLabels: false,
	checkPhotoNumbering: false,
	checkPhotoNumberingGently: true
    },
    initialize: function (options) {
        this.setOptions(options);
        var group_name = this.options.groupConfig.groupname();
        // layout style
        var storedUCPStyle = GM_getValue("UCP.ucpStyle." + group_name);
        this.options.ucpStyle = (storedUCPStyle === true || storedUCPStyle === 'true');
        // language
        var storedLanguage = GM_getValue("UCP.language." + group_name);
        if (storedLanguage) {
                this.options.language = storedLanguage;
        }
        // legacy labels
        var storedUseLegacyLabels = GM_getValue("UCP.useLegacyIcons." + group_name);
        this.options.useLegacyLabels = storedUseLegacyLabels == true || storedUseLegacyLabels == 'true';
        
	// photo number checking
	var storedCheckPhotoNumbering = GM_getValue("UCP.checkPhotoNumbering." + group_name);
	this.options.checkPhotoNumbering = storedCheckPhotoNumbering == true || storedCheckPhotoNumbering == 'true';
	
	var storedCheckPhotoNumberingGently = GM_getValue("UCP.checkPhotoNumberingGently." + group_name);
	this.options.checkPhotoNumberingGently = storedCheckPhotoNumberingGently == true || storedCheckPhotoNumberingGently == 'true';
    },
    groupConfig: function () {
        return this.options.groupConfig;
    },
    setStyle: function (ucpStyle) {
	if (ucpStyle !== this.ucpStyle()) {
            GM_setValue("UCP.ucpStyle." + this.groupConfig().groupname(), ucpStyle);
	    this.options.ucpStyle = ucpStyle;
        }
    },
    setLanguage: function (language) {
        if (language !== this.language()) {
            GM_setValue("UCP.language." + this.groupConfig().groupname(), language);
            this.options.language = language;
        }
    },
    setUseLegacyLabels: function (useLegacyLabels) {
        if (useLegacyLabels !== this.useLegacyLabels()) {
            GM_setValue("UCP.useLegacyIcons." + this.groupConfig().groupname(), useLegacyLabels);
            this.options.useLegacyLabels = useLegacyLabels;
        }
    },
    setCheckPhotoNumbering: function (b) {
	if (b !== this.checkPhotoNumbering()) {
		GM_setValue("UCP.checkPhotoNumbering." + this.groupConfig().groupname(), b);
		this.options.checkPhotoNumbering = b;
	}
    },
    setCheckPhotoNumberingGently: function (b) {
	if (b !== this.checkPhotoNumberingGently()) {
		GM_setValue("UCP.checkPhotoNumberingGently." + this.groupConfig().groupname(), b);
		this.options.checkPhotoNumberingGently = b;
	}
    },
    // shortcuts
    useLegacyLabels: function () {
        return this.options.useLegacyLabels;
    },
    checkPhotoNumbering: function () {
	return this.options.checkPhotoNumbering;
    },
    checkPhotoNumberingGently: function () {
	return this.options.checkPhotoNumberingGently;
    },
    language: function() {
        return this.options.language;
    },
    ucpStyle: function () {
        return this.options.ucpStyle;
    }
});

function ucpCheckPhoto(photoNode, index, array) { // expects 'this' to be of type UCPChallengeDefinition
    try {
	    var src = photoNode.getAttribute('src');
        if (!$chk(src) || src.length === 0) {
            return false;
        }
	try {
            if (photoNode.getAttribute('alt') === 'UCPthumbnail') {
                return false;
            }
            if (photoNode.getAttribute('alt').match('UCPANG:bump')) {
	        return false;
            }
	} catch (e) {
	    // ignore
	}
        if (src.contains("buddyicons")) {
            return false;
        } 
	if (src.contains("coverphoto")) {
		return false;
	}
        if (!src.match(/static\.?flickr\.com/) && !src.match(/.*yimg\.com/)) {
            return false;
        }
	if (!src.match(/.*\/\d+_[\da-z]+(?:_\w)?\.jpg/)) {
		return false;
	}
        if (!this.iconChallenge()) {
            // ignore the height attribute: Flickr adds class="notsowide":
            // img.notsowide: {
            //     height: auto;
            //     max-width: 500px;
            //  }
            //  => even if specified, 'height' does nothing
	    try {
                var width = photoNode.getAttribute('width');
                // this.width returns bogus info if attribute 'width' is missing
                if ($chk(width) && width < 275) {
                    return false;
                }
	    } catch (e) {
	        // ignore
            }
            // thumbnails, square, .. from flickr are not medium
            if (src.match(/_t.jpg$|_s.jpg$/)) {
                return false;
            }
        }
        if ($chk(global_group_config)) {
            if ($chk(global_group_config.nonPhotoImages()[src])) {
                return false;
            }
        }
        return true;
    } catch (e) {
        GM_log("error checking photo: " + e);
        return false;
    }
	return false;
}

// from http://pmav.eu/stuff/javascript-hashing-functions/source.html
function ucpUniversalHash(s, tableSize) {
    if (!tableSize) {
        tableSize = 65534;
    }
    var b = 27183, h = 0, a = 31415;

    if (tableSize > 1) {
        for (var i = 0; i < s.length; i++) {
            h = (a * h + s[i].charCodeAt()) % tableSize;
            a = ((a % tableSize) * (b % tableSize)) % (tableSize);
        }
    }
    return h;
}

var UCPVote = new Class({
    Implements: [Options],
    options: {
        chlgname: null,
        ucpThread: null,
        node: null,
	messageId: null,
        poster: null,
        voteText: null,
        votesArray: [],
        messages: [],
        errorIdx: null,
        votedFor: []
    },
    initialize: function (options) {
	for (var key in options) {
		this.options[key] = options[key];
	}
        this.maxVote = 0;
        if (options.votesArray !== null) {
            for (var oIdx = 0, oLen = options.votesArray.length; oIdx < oLen; oIdx++) {
                if (!isNaN(options.votesArray[oIdx])) {
                    this.maxVote = Math.max(options.votesArray[oIdx], this.maxVote);
                    this.options.votesArray[oIdx] = options.votesArray[oIdx];
                } else {
                    this.options.votesArray[oIdx] = 0;
                }
            }
        }
    },
    poster: function () {
        return this.options.poster;
    },
    voteText: function () {
        return this.options.voteText;
    },
    node: function () {
        return this.options.node;
    },
    ucpThread: function () {
        return this.options.ucpThread;
    },
    isUnanimous: function () {
        var scoreIdx = 0;
        for (var oIdx = 1, oLen = this.options.votesArray.length; oIdx < oLen; ++oIdx) {
            if (!isNaN(this.options.votesArray[oIdx]) && this.options.votesArray[oIdx] > 0) {
                if (scoreIdx === 0) { // first vote found
                    scoreIdx = oIdx;
                } else {
                    if (scoreIdx !== oIdx) {
                        return false;
                    }
                }
            }
        }
        return scoreIdx !== 0; // no score found = no voting = not unanimous
    },
    isVoid: function () {
        return false;
    },
    add: function (other) {
        other.options.votesArray.each(function (vote, oIdx) {
            if (oIdx === 0) {
                return;
            }
            if (vote && !isNaN(vote)) {
                if (this.options.votesArray[oIdx] && !isNaN(this.options.votesArray[oIdx])) {
                    this.options.votesArray[oIdx] += vote;
                } else {
                    this.options.votesArray[oIdx] = vote;
                }
                if (this.options.votesArray[oIdx] > this.maxVote) {
                    this.maxVote = this.options.votesArray[oIdx];
                }
            }
        }, this);
    },
    topDiff: function () { // returns the difference between the two top scores
        var secondBest = 0;
        // this.maxVote should be set, but we don't have its index
        var theBest = 0;
        this.options.votesArray.each(function (score, oIdx) {
            if (oIdx === 0) {
                return;
            }
            if (score > theBest) {
                secondBest = theBest;
                theBest = score;
            } else if (score > secondBest) {
                secondBest = score;
            }
        }, this);
        return theBest - secondBest;
    },
    toString: function () {
        var retval = this.options.poster.username + ' ' + this.options.voteText + ' =(';
        this.options.votesArray.each(function (vote, idx) {
	    if (idx == 0) return;
            retval = retval + ' ' + vote;
        }, this);
        retval = retval + ') => ' + this.maxVote;
        return retval;
    },
    votedFor: function () {
        var votedForString = "";
        if (this.options.votedFor.length > 0) {
            votedForString = "(";
            this.options.votedFor.each( function(value,idx) {
                if (idx == 0) return;
                if (value != undefined && value > 0) {
                    if (votedForString.length > 1) votedForString += ",";
                    votedForString += (idx);
                }
            });
            votedForString += ")";
        }
        return votedForString;
    },
    showVotes: function (sort) {
        var retval = undefined;
        // create an array with objects {idx,vote}
        var showArray = this.options.votesArray.map( function (vote, idx) {
            return { photo: idx, points: vote };
        });
        if (sort) {
            // sort on points
            showArray.sort(function (a,b) {
                return b.points - a.points;
            });
        }
        showArray.each(function (vote, oIdx) {
            if (isNaN(vote.points) || vote.points <= 0) {
                return;
            }
            var part = vote.photo + ":" + vote.points + "pt";
            if (this.options.errorIdx === vote.photo) {
                part = "<b>" + part + "</b>";
            }
            if (retval === undefined) {
                retval = part;
            } else {
                retval = retval + ", "  + part;
            }
        }, this);
        return "(" + retval + ")";
    },
    showPicResult: function (sort) {
        // first, sort the result
        var sorted = [];
        var vote;
        for (var oIdx = 1, oLen = this.options.votesArray.length; oIdx < oLen; ++oIdx) {
            vote = this.options.votesArray[oIdx];
            if (!isNaN(vote)) {
                var added = false;
                for (var sIdx = 0, sLen = sorted.length; sIdx < sLen; ++sIdx) {
                    if (sorted[sIdx].value <= vote) {
                        // insert element => move the rest to the end
                        for (var rIdx = sorted.length; rIdx > sIdx; --rIdx) {
                            sorted[rIdx] = sorted[rIdx - 1];
                        }
                        sorted[sIdx] = { photo: oIdx, value: vote };
                        added = true;
                        break;
                    }
                }
                if (!added) {
                    sorted[sorted.length] = { photo: oIdx, value: vote };
                }
            }
        }
        var retval = "";
        sorted.each(function (unsorted_vote) {
            retval = retval + (retval.length > 0 ? ", " : "") + unsorted_vote.photo + ":" + unsorted_vote.value;
        }, this);
        return "(" + retval + ")";
    },
    messages: function() {
        return this.options.messages;
    },
    addMessage: function (message) {
        this.options.messages.include({msg: message, type: 'message'});
    },
    addWarning: function (warning) {
        this.options.messages.include({msg: warning, type: 'warning'});
    },
    hasWarning: function () {
        return this.options.messages.some( function (msg) {
            return msg.type == 'warning';
	});
    },
    warning: function() {
        var retval = '';
        this.options.messages.each(function (message) {
            if (message.type === 'warning') {
                if (retval.length > 0) retval += " - ";
                retval += message.msg;
            }
        });
        return retval;
    },
    hasError: function () {
        return this.options.messages.some( function (msg) {
            return msg.type == 'error';
        });
    },
    addError: function (error) {
        this.options.messages.include({msg: error, type: 'error'});
    },
    error: function() {
        var retval = '';
        this.options.messages.each(function (message) {
            if (message.type === 'error') {
                if (retval.length > 0) retval += " - ";
                retval += message.msg;
            }
        });
        return retval;
    },
    printStatus: function () {
        if (this.options.ucpThread.options.needsStatusOnChallengePage) {
            if ($chk(this.messages()) && this.messages().length > 0) {
		if ($chk(this.options.messageId)) { // the challenge announcement does not have an entry_id
			var pageAnchor = $$('td.Who a[name=comment' + this.options.messageId + ']')[0].getParent('tr').getElements('small').getLast();
		} else {
			if (!global_group_preferences.groupConfig().allowsPhotoInAnnouncement()) {
				GM_log("ERROR: found a photo entry without message id; skipping");
				return;
			}
			pageAnchor = $$('td.Who')[0].getParent('tr').getElements('small').getLast();
		}
                if ($chk(pageAnchor) && $chk(pageAnchor.getParent('div.ucpdiv'))) {
                    pageAnchor = pageAnchor.getParent('div.ucpdiv');
                }
                if (!$chk(pageAnchor)) {
                    pageAnchor = this.node();
                }
                
                var commentAnchor = new Element('small', {
                    html: "UCheckPlayNG: "
                }).inject(new Element('div', {
                    'class': 'ucpdiv'
                }).inject(pageAnchor, 'after'));
                this.messages().each(function (message, idx) {
                    var msg = (idx > 0 ? " - " : "") + message.msg;
                    var color = message.type === 'error' ? 'red' : message.type === 'warning' ? 'orange' : '';
                    commentAnchor.adopt(new Element('span', {
                        html: msg,
                        styles: {
                            color: color
                        }
                    }));
                });
            }
        }
    }
});

var UCPVerticalVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) { 
        // numbers in vote == nPhotos ??
        var challengeConfig = this.ucpThread().challengeDefinition();
        if (challengeConfig.neededPhotos() > 0 && nPhotos == challengeConfig.neededPhotos()) {
            // we need an entry for each vote, numbered sequentially
            if (this.options.votesArray.length - 1 < nPhotos) {
                this.addError('did not provide a vote line for every photo');
                return false;
            }
            if (this.options.votesArray.length - 1 > nPhotos) {
                this.addError('provided too much vote lines');
                return false;
            }
        }
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        if (!previousVote || (voteCommentNumber == 1 && previousVote.isExampleVote())) { // first (real) vote
            // unless it is an example vote, the first vote entry should contain only 1 vote
            var nVotes = this.options.votesArray.filter( function(vote, idx) {
                if (vote && vote > 0) {
                    this.options.votedFor[idx] = vote;
                    return true;
                }
                return false;
            }, this).length;
            if (nVotes > 1) {
                this.addError('voted on more than 1 photo');
                return false;
            }

            var maxOne = this.options.votesArray.every( function(vote, idx) {
                if (idx == 0) return true;
                if (vote == undefined) return true;
                if (vote > 1) return false;
                return true;
            });
            if (!maxOne) {
                this.addError('voted too much for the same photo');
                return false;
            }
        } else { // compare with previous vote: all votes should remain the same, except for one increment
	    nVotes = 0;
            this.options.votedFor = [];
            this.options.votesArray.each( function(vote, idx) {
                var prevVote = previousVote.options.votesArray[idx];
                if (idx == 0) return;
                if (vote == undefined && prevVote == undefined) return;
                if (prevVote != undefined) {
                    if (vote == undefined) {
                        this.addError('dropped a vote on photo ' + idx);
                    } else {
                        if (vote > ( prevVote + 1 )) {
                            this.options.votedFor[idx] = vote;
			    ++nVotes;
                            this.addError('voted too much on photo ' + idx);
                        } else if (vote < prevVote) {
                            this.addError('dropped a vote on photo ' + idx);
                        } else if (vote == (prevVote + 1)) {
                            this.options.votedFor[idx] = vote;
			    ++nVotes;
                        }
                    }
                } else {
                    if (vote > 1) {
                        this.options.votedFor[idx] = vote;
			++nVotes;
                        this.addError('voted too much on photo ' + idx);
                    }
                }
            }, this);
            if (this.options.votedFor.length == 0) {
                this.addError('did not really vote');
            }
	    if (nVotes > 1) {
	    	this.addError('voted for ' + nVotes + ' photos');
	    }
            if (!this.hasError()) {
                if (this.options.votesArray.length !== previousVote.options.votesArray.length) {
                    this.addError('but provided ' + (this.options.votesArray.length - 1) + ' vote lines while \'' + 
                            previousVote.poster().username + '\' provided ' + (previousVote.options.votesArray.length - 1));
                }
            }
            if (this.hasError()) return false;
        }
        
        return true;
    },
    isExampleVote: function () {
        // in VERTICAL, the example vote is empty
        return this.options.votesArray.every( function (vote, idx) {
            if (idx == 0) return true;
            return !vote || vote == 0;
        });
    }
});

var UCPHorizontalVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) {
        this.options.votedFor = [];
        // numbers in vote == nPhotos ??
        var challengeConfig = this.ucpThread().challengeDefinition();
        if (challengeConfig.neededPhotos() > 0 && nPhotos == challengeConfig.neededPhotos()) {
            // we need an entry for each vote, numbered sequentially
            if (this.options.votesArray.length - 1 < nPhotos) {
                this.addError('did not provide a vote for every photo');
                return false;
            }
            if (this.options.votesArray.length - 1 > nPhotos) {
                this.addError('provided too much votes ');
                return false;
            }
        }
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        if (!previousVote || (voteCommentNumber == 1 && previousVote.isExampleVote()) &&
            this.ucpThread().challengeDefinition().scoresAdded()) { // first (real) vote
            // unless it is an example vote, the first vote entry should contain only 1 vote (0-0-1)
            var nVotes = this.options.votesArray.filter( function(vote, idx) {
                if (vote && vote > 0) {
                    this.options.votedFor[idx] = vote;
                    return true;
                }
                return false;
            }, this).length;
            if (nVotes > 1) {
                this.addError('voted on more than 1 photo');
                return false;
            }
            var maxOne = this.options.votesArray.every( function(vote, idx) {
                if (idx == 0) return true;
                if (vote == undefined) return true;
                if (vote > 1) return false;
                return true;
            });
            if (!maxOne) {
                this.addError('voted too much for the same photo');
                return false;
            }
        } else { // compare with previous vote: all votes should remain the same, except for one increment/decrement
	    nVotes = 0;
            if (this.ucpThread().challengeDefinition().scoresAdded()) {
                this.options.votesArray.each( function(vote, idx) {
                    var prevVote = previousVote.options.votesArray[idx];
                    if (idx == 0) return;
                    if (vote == undefined && prevVote == undefined) return;
                    if (prevVote != undefined) {
                        if (vote == undefined) {
                            this.addError('dropped a vote on photo ' + idx);
                        } else {
                            if (vote > ( prevVote + 1 )) {
                                this.options.votedFor[idx] = vote;
				++nVotes;
                                this.addError('voted too much on photo ' + idx);
                            } else if (vote < prevVote) {
                                this.addError('dropped a vote on photo ' + idx);
                            } else if (vote == (prevVote + 1)) {
                                this.options.votedFor[idx] = vote;
				++nVotes;
                            }
                        }
                    } else {
                        if (vote > 1) {
                            this.options.votedFor[idx] = vote;
			    ++nVotes;
                            this.addError('voted too much on photo ' + idx);
                        }
                    }
                }, this);
            } else { // constructive challenges
                this.options.votesArray.each( function(vote, idx) {
                    var prevVote = previousVote.options.votesArray[idx];
                    if (idx == 0) return;
                    if (vote == undefined && prevVote == undefined) return;
                    if (prevVote != undefined) {
                        if (vote == undefined) {
                            this.addError('dropped a vote on photo ' + idx);
                        } else {
                            if (vote < ( prevVote - 1 )) {
                                this.options.votedFor[idx] = vote;
				++nVotes;
                                this.addError('voted too much on photo ' + idx);
                            } else if (vote > prevVote) {
                                this.addError('dropped a vote on photo ' + idx);
                            } else if (vote == (prevVote - 1)) {
                                this.options.votedFor[idx] = vote;
				++nVotes;
                            }
                        }
                    } else {
                        if (vote < 5) {
                            this.options.votedFor[idx] = vote;
			    ++nVotes;
                            this.addError('voted too much on photo ' + idx);
                        }
                    }
                }, this);
            }
            if (this.options.votedFor.length == 0) {
                this.addError('did not really vote');
            }
	    if (nVotes > 1) {
	    	this.addError('voted for ' + nVotes + ' photos');
	    }
            if (!this.hasError()) {
                if (previousVote && this.options.votesArray.length !== previousVote.options.votesArray.length) {
                    this.addError('but provided ' + (this.options.votesArray.length - 1) + ' votes while \'' + 
                            previousVote.poster().username + '\' provided ' + (previousVote.options.votesArray.length - 1));
                }
            }
            if (this.hasError()) return false;
        }
        return true;
    },
    isExampleVote: function () {
        // in HORIZONTAL, the example vote is 1-2-3
        // unless in constructive challenges, which start at 5-5-5
        if (!this.ucpThread().challengeDefinition().scoresAdded()) {
            return this.options.votesArray.every( function (vote, idx) {
                if (idx == 0) return true;
                return vote == 5;
            });
        }
        return this.options.votesArray.every( function (vote, idx) {
            if (idx == 0) return true;
            return vote == idx;
        });
    }
});

var UCPVerticalWeightedVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) { 
        var challengeConfig = this.ucpThread().challengeDefinition();
        // numbers in vote == nPhotos ??
        if (challengeConfig.neededPhotos() > 0 && nPhotos == challengeConfig.neededPhotos()) {
            // we need an entry for each vote, numbered sequentially
            if (this.options.votesArray.length - 1 < nPhotos) {
                this.addWarning('did not provide a vote line for every photo');
            }
            if (this.options.votesArray.length - 1 > nPhotos) {
                this.addWarning('provided too much vote lines');
            }
        }
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        if (!previousVote || (voteCommentNumber == 1 && previousVote.isExampleVote())) { // first (real) vote
            // unless it is an example vote, the first vote entry should contain only 3 votes
            var nVotes = this.options.votesArray.filter( function(vote, idx) {
                if (vote && vote > 0) {
                    this.options.votedFor[idx] = vote;
                    return true;
                }
                return false;
            }, this).length;
            if (nVotes < 3) {
                this.addError('voted on only ' + nVotes + ' photo' + (nVotes > 1 ? 's' : ''));
                return false;
            }
            if (nVotes > 3) {
                this.addError('voted on ' + nVotes + ' photos');
                return false;
            }

            // the first vote should contain 3 ratings: 1, 2 and 3
            var ratings = this.options.votesArray.filter( function(vote, idx) {
                if (idx == 0) return false;
                if (vote && vote > 0) {
                    this.options.votedFor[idx] = vote;
                    return true;
                }
                return false;
            }, this);
            if (ratings.length > 3) {
                this.addError('provided too much ratings');
                return false;
            }
            if (ratings.length < 3) {
                this.addError('did not provide enough ratings');
                return false;
            }
            var validRatings = this.options.votesArray.filter( function(vote, idx) {
                if (idx == 0) return false;
                if (vote && (vote == 3 || vote == 2 || vote == 1)) return true;
                return false;
            });
            // there can not be more than 3: already filtered in above test
            if (validRatings.length < 3) {
                this.addError('did not provide enough valid ratings');
                return false;
            }
            // check for duplicates
            validRatings.sort(function(a,b){return a - b ;});
            if (!validRatings.every(function (value, idx) { return value == idx + 1; })) {
                this.addError('provided duplicate ratings');
                return false;
            }
        
        } else { // compare with previous vote: all votes should remain the same, except for 3 ratings
            var voteArray = [];
            voteArray[1] = voteArray[2] = voteArray[3] = 0;
            this.options.votesArray.each( function(vote, idx) {
                if (idx == 0) return;
                var prevVote = previousVote.options.votesArray[idx];
                if ((vote == undefined || vote == 0) && (prevVote == undefined || prevVote == 0)) return;
                if (prevVote == undefined || prevVote == 0) { // first vote on this photo
                    this.options.votedFor[idx] = vote;
                    if (vote > 3) {
                        this.addError('voted too much on photo ' + idx);
                        return;
                    }
                    if (vote < 1) {
                        this.addError('voted negative on photo ' + idx);
                        return;
                    }
                    voteArray[vote] = ++voteArray[vote];
                } else {
                    if (prevVote == vote) return;
                    // new ratings on this one
                    voteArray[vote - prevVote] = ++voteArray[vote - prevVote];
                    this.options.votedFor[idx] = vote - prevVote;
                    if ((vote - prevVote) > 3) {
                        this.addError('voted too much on photo ' + idx);
                        return;
                    }
                    if (prevVote > vote) {
                        this.addError('dropped a vote on photo ' + idx);
                        return;
                    }
                }
            }, this);
            if (!this.hasError()) {
                if (this.options.votesArray.length !== previousVote.options.votesArray.length) {
                    this.addWarning('but provided ' + (this.options.votesArray.length - 1) + ' vote lines while \'' + 
                            previousVote.poster().username + '\' provided ' + (previousVote.options.votesArray.length - 1));
                }
            }
            if (!voteArray.every(function(vote,idx){
                if (idx == 0) return true;
                return vote == 1;
            })) {
                this.addError('invalid rating');
                // TODO: show doubles or missing ones
                GM_log("invalid rating: " + voteArray);
            }
        }
        
        return !this.hasError();
    },
    isExampleVote: function () {
        // in VERTICAL-WEIGHTED, the example vote is empty
        return this.options.votesArray.every( function (vote, idx) {
            if (idx == 0) return true;
            return !vote || vote == 0;
        });
    },
    votedFor: function () {
        // the same as for the base class, but the weight should show
        var votedForString = "";
        if (this.options.votedFor.length > 0) {
            votedForString = "(";
            this.options.votedFor.each( function(value,idx) {
                if (idx == 0) return;
                if (value != undefined && value > 0) {
                    if (votedForString.length > 1) votedForString += ",";
                    votedForString += (idx + ":" + value + "pt");
                }
            });
            votedForString += ")";
        }
        return votedForString;
    }
});

var UCPRatePhotoVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) {
        this.options.votedFor = [];
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        this.options.votesArray.each( function(vote, idx) {
            if (vote && vote > 0) {
                this.options.votedFor[idx] = vote;
            }
        }, this);
            // none of the votes should be for a non-existing photo
            // => does not work: if photo 14 is removed from the challenge, later entries may keep their number
        return true;
    },
    isExampleVote: function () {
        // 1 - 3pt
	// 2 - 2pt
	// 3 - 3pt
	// => votesArray (0,3,2,1)
	return this.options.votesArray.every( function (value, idx) {
		if (idx == 0) return true;
		return (idx + value) == 4;
	});
    },
    votedFor: function () {
        // the same as for the base class, but the weight should show
        var votedForString = "";
        if (this.options.votedFor.length > 0) {
            votedForString = "(";
            this.options.votedFor.each( function(value,idx) {
                if (idx == 0) return;
                if (value != undefined && value > 0) {
                    if (votedForString.length > 1) votedForString += ",";
                    votedForString += (idx + ":" + value + "pt");
                }
            });
            votedForString += ")";
        }
        return votedForString;
    }
});

var UCPPICHorizontalVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) {
        this.options.votedFor = [];
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        this.options.votesArray.each( function(vote, idx) {
            if (vote && vote > 0) {
                this.options.votedFor[idx] = 1;
            }
        }, this);
            // none of the votes should be for a non-existing photo
            // => does not work: if photo 14 is removed from the challenge, later entries may keep their number
        return true;
    },
    isExampleVote: function () {
        return false;
    }
});

var UCPPICVerticalVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) {
        this.options.votedFor = [];
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        this.options.votesArray.each( function(vote, idx) {
            if (vote && vote > 0) {
                this.options.votedFor[idx] = 1;
            }
        }, this);
            // none of the votes should be for a non-existing photo
            // => does not work: if photo 14 is removed from the challenge, later entries may keep their number
        return true;
    },
    isExampleVote: function () {
        return false;
    }
});

var UCPPICPlayerVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) {
        this.options.votedFor = [];
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        this.options.votesArray.each( function(vote, idx) {
            if (vote && vote > 0) {
                this.options.votedFor[idx] = 1;
            }
        }, this);
            // none of the votes should be for a non-existing photo
            // => does not work: if photo 14 is removed from the challenge, later entries may keep their number
        return true;
    },
    isExampleVote: function () {
        return false;
    }
});

function extractPhotoId(srcUrl) {
	// old url http://www.flickr.com/123/1234_5678.jgp
	// old url http://farmX.staticflickr.com/123/1234_5678.jpg
	// new url http://c5.staticflickr.com/sm/123/1234_5678.jpg
	var match = srcUrl.match(/https?:\/\/[^\/]*flickr.com\/.+\/(\d+)_.*/);
	if (match) {
		return match[1];
	}
	// new url http://v4s2.yimg.com/so/1234/1234_5678.jpg"
	match = srcUrl.match(/https?:\/\/[^\/]*yimg.com\/.*\/(\d+)_.*/);
	if (match) {
		return match[1];
	}
	return null;
}

function ucpCheckPhotoApproval(photo, topic) {
// TODO: 
// - fails in case there two or three photo entries: 
// http://www.flickr.com/groups/thumbsup_challenges/discuss/72157625215597037/
        // returns
        // {
        //      approved: true/false,
        //      approver: string,
        //      version: number,
        //      checksum: true/false,
        //      photoChecksum: true/false,
        //      error: string
        //      photoId: string
        //  }

	var photoTextNode = photo;
	while ($chk(photoTextNode.getParent('div'))) {
		photoTextNode = photoTextNode.getParent('div');
	}
    
    var photoId = extractPhotoId(photo.get('src'));
/*return {
        approved: true,
        approver: "todo",
        photoId: photoId
    }; */
    try {
        var approvedNode = photoTextNode.getElement("img[src=http://l.yimg.com/g/images/spaceout.gif][alt*=UCPAapproved]");
    } catch (e) {
        GM_log("error: " + e);
        return true;
    }
    if (!approvedNode) {
        return { approved: false };
    }
    var approvedString = approvedNode.get('alt');
    var ignore, name, photoChecksum, checksum, version;
    try {
        var approveMatch = /UCPAapproved:([^:]*):([^:]*):([^:]*):(\d+)/.exec(approvedString);
        ignore = approveMatch[0];
        name = decodeURIComponent(approveMatch[1]);
        photoChecksum = approveMatch[2];
        checksum = approveMatch[3];
        version = approveMatch[4];
    } catch (e) {
        return { approved: false,
                 error: "checksum has been modified!",
                 photoId: photoId
        };
    }
    if ($chk(ignore)) {
        var goodPhotoChecksum = ucpUniversalHash(photo.src);
        if (goodPhotoChecksum !== parseInt(photoChecksum, 10)) {
            return { approved: false,
                     version: version,
                     checksum: true,
                     photoChecksum: false,
                     error: "photo has been changed after approval!",
                     photoId: photoId
            };
        }
        var goodChecksum = ucpUniversalHash(photoId + name + topic);
        //GM_log("good: " + goodChecksum + " - photo: " + checksum);
        if (goodChecksum !== parseInt(checksum, 10)) {
            return { approved: false,
                     version: version,
                     checksum: false,
                     error: "checksum has been tampered with!",
                     photoId: photoId
            };
        }
        return { 
            approved: true,
            approver: name,
            version: version,
            checksum: true,
            photoChecksum: true,
            photoId: photoId
        };
    }
    return { approved: false, error: "failed to process UCPA approved entry"};
}

function parseVerticalVote(data) {
    var replytxt = data.replytxt;
    var challengeEntry = data.challengeEntry;
    var messageId = data.messageId;
    var ucpThread = data.ucpThread;
    var challengeConfig = ucpThread.challengeDefinition();
    var poster = data.poster;
    var exampleVote = data.exampleVote;

    var vVotesArray = [];
    var vVoteLines = replytxt.split(/\n\s*/);
    // voteLines is an array:
    // #01- 1
    // #02- 1+2+3
    // ..
    // others use 1:
    var reVoteMatch1 = /(\d{1,2})#/; // 01-
    var reVoteMatch2 = /(\d{1,2})#(\d+)/; // '01-1'
    var reVoteMatch3 = /(\d{1,2})#(\d+)([^0-9azA-Z](\d+))+/; // '01-1+2' && '01- 1+2+3'
    var vVotesFound = 0;
    var vVoided = false;
    // it misses the first votes: 01-1 or 06-1, but that's to be ignored :)
    for (var j = 0, vVoteLinesLength = vVoteLines.length; j < vVoteLinesLength; ++j) {
        var vVoteLine = vVoteLines[j];
        // remove any leading spaces
        vVoteLine = vVoteLine.replace(/^\s*/, '');
        // ignore lines that are comment text
        if (vVoteLine.match(/^\s*[a-zA-Z\s\.\,\(]{6}/)) { // 6? arbitrary
            continue;
        }
        // first seperate the photo number from the votes, in case it was seperated with a space
        var reVReplaceMatch = /#(\d{1,2})/;
        vVoteLine = vVoteLine.replace(reVReplaceMatch, "$1"); // remove leading #
        reVReplaceMatch = /(\d{1,2})([\-:+\. ]+|\.{2,3})/; // faves contest uses '...', adding it to [] creates havoc in other places, even with escaping (or escaping double)
        vVoteLine = vVoteLine.replace(reVReplaceMatch, "$1#"); // insert # after photonumber
        // remove spaces, but first replace spaces between votes, if any
        vVoteLine = vVoteLine.replace(/(\d)\s+(\d)/g, "$1+$2");
        vVoteLine = vVoteLine.replace(/\s/g, '');
        // remove any character before first vote in voteLine 00#-1+2
        vVoteLine = vVoteLine.replace(/(\d{1,2}#)[^0-9]*/, "$1");
        if (vVoteLine.match(/^\s*\d{1,2}\s*$/)) {
            vVoteLine = vVoteLine.replace(/^\s*(\d{1,2})\s*$/, "$1#0");
        }
        if (vVoteLine.match(/void/i) && !vVoteLine.match(/^\d/)) {
            // Skip replies where the vote has been voided
            vVoided = true;
            break;
        }
        var photoNumber, photoScore;
        var verticalVotes = reVoteMatch3.exec(vVoteLine);
        if (verticalVotes) {
            photoNumber = verticalVotes[1];
            photoScore = verticalVotes[4];
            vVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(verticalVotes[4], 10);
            ++vVotesFound;
            continue;
        }
        verticalVotes = reVoteMatch2.exec(vVoteLine);
        if (verticalVotes) {
            photoNumber = verticalVotes[1];
            photoScore = verticalVotes[2];
            vVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(photoScore, 10);
            ++vVotesFound;
            continue;
        }
        verticalVotes = reVoteMatch1.exec(vVoteLine);
        if (verticalVotes) {
            photoNumber = verticalVotes[1];
            vVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = 0;
            ++vVotesFound;
            continue;
        }
        // thisvotes is still null => normal comment
    }       
    //GM_log(vVotesArray);
    if (vVoided) {
        var voidedVote = new UCPVote({
            poster: poster, 
            voteText: vVoteLines, 
            votesArray: vVotesArray, 
            node: challengeEntry,
	    messageId: messageId,
            ucpThread: ucpThread
        });
        voidedVote.isVoid = function () {
            return true;
        };
        return voidedVote;
    }
    if (vVotesFound <= 1) { // voting for one photo makes no sense
    	return new UCPVoteComment({
    		poster: poster,
    		node: challengeEntry,
    		messageId: messageId,
    		ucpThread: ucpThread
    	});
    }
    var vote = new UCPVerticalVote({
    	chlgname: challengeConfig.reName(), 
    	poster: poster, 
    	voteText: vVoteLines, 
    	votesArray: vVotesArray, 
    	node: challengeEntry,
    	messageId: messageId,
    	ucpThread: ucpThread
    });
    if (exampleVote) {
    	vote.isExampleVote = function () {
    		return true;
    	};
    }
    return vote;
}

function parseHorizontalVote(data) {
    var ucpThread = data.ucpThread;
    var picType = ucpThread.challengeDefinition().scoreType().match('PIC-H');
    var exampleVote = data.exampleVote;
    var poster = data.poster;
    var challengeEntry = data.challengeEntry;
    var messageId = data.messageId;
    var replytxt = data.replytxt;

    // first: cleanup the comment, to ease the vote recognition

    // in The mother of all challenge group, the 'local' CP script has no way to handle '10'; 
    // they use ten instead
    // people that use this hack, also use it in other groups:
    replytxt = replytxt.replace(/[\s,-]+ten[\s,-]+/g,  "-10-");
    replytxt = replytxt.replace(/[\s,-]+nine[\s,-]+/g,  "-9-");
    replytxt = replytxt.replace(/[\s,-]+eight[\s,-]+/g, "-8-");
    replytxt = replytxt.replace(/[\s,-]+seven[\s,-]+/g, "-7-");
    replytxt = replytxt.replace(/[\s,-]+six[\s,-]+/g,   "-6-");
    replytxt = replytxt.replace(/[\s,-]+five[\s,-]+/g,  "-5-");
    replytxt = replytxt.replace(/[\s,-]+four[\s,-]+/g,  "-4-");
    replytxt = replytxt.replace(/[\s,-]+three[\s,-]+/g, "-3-");
    replytxt = replytxt.replace(/[\s,-]+two[\s,-]+/g,   "-2-");
    replytxt = replytxt.replace(/[\s,-]+one[\s,-]+/g,   "-1-");
    replytxt = replytxt.replace(/[\s,-]+nil[\s,-]+/g,   "-0-");
    replytxt = replytxt.replace(/[\s,-]+zero[\s,-]+/g,  "-0-");
    replytxt = replytxt.replace(/[\s,-]+o[\s,-]+/g,     "-0-");
    var replyLines = replytxt.split(/\n\s*/);
    var horizontalVotes = null;
    $each(replyLines, function (replyLine) {
        if (replyLine.match(/void/i)) {
            // Skip lines where the vote has been voided
            return;
        }
        // check for comments before any other manipulations
        // corrections of the form 'corrected:', 'correction:' (need the ':' -> '1-2-3 with correction')
        replyLine = replyLine.replace(/cor(?:r)?ected:|correction:/, '&gt;&gt;'); // corrected sometimes mis-spelled
//      removed: vote after misvote '1-2-3 (with correction)' where seen as regular comments
        if (!replyLine || replyLine.replace(/<[^>]*>/g, '')          // remove html tags
                                       .replace(/^\s*/g, '')             // remove leading spaces
                                       .replace(/^foto[^\d]*|^photo[^\d]*/i, '')     // remove 'photo' in 'photo 2: 1-1-1'
                                       .replace(/(&gt;)+/, '') // don't ignore corrections (>> or -->)
                                       .match(/^[^\d]{6}/)) { // 6? arbitrary
            return;
        }
        replyLine = replyLine.replace(/<a[^<]*<\/a>/g, ''); // removes links
        replyLine = replyLine.replace(/\s*<[^>]*>\s*/g, ''); // removes html tags

        // remove any leading white space
        replyLine = replyLine.replace(/^\s*/g, '');
        replyLine = replyLine.replace(/^foto[^\d]*|^photo[^\d]*/i, '');
        if (!replyLine) {
            return;
        }
        replyLine = replyLine.replace(/^.*(?:&gt;|\||→)+\s*/g, ''); // correction
        // some like voting with extra spaces '0 - 1 - 2'
        // must be done after correction characters
        //replyLine = replyLine.replace(/(\d+)\s+[^a-zA-Z0-9]/g, "$1-");
        // remove the trailing '-'
        replyLine = replyLine.replace(/-$/, '');
        // ignore lines that are comment text
        if (!replyLine || replyLine.match(/^\s*[a-zA-Z\s\.\,\(]{6}/)) { // 6? arbitrary
            return;
        }

        // catches multiple votes, or comment, on multiple lines
        var matchVotes = ucpThread.reHorizontalVoteMatch().exec(replyLine);
        while (matchVotes) { // catches multiple votes (correction?) on one line
            horizontalVotes = matchVotes;
            matchVotes = ucpThread.reHorizontalVoteMatch().exec(replyLine);
        }
    });
    if (horizontalVotes) {
        var hVotesArray = [];
        var voteIdx     = 1; // 0: the input string
        while (!horizontalVotes[voteIdx]) {
            ++voteIdx; // skip the non matching part
        }
        var arrayIdx = 1;
        while (true) {
            if (picType) {
                hVotesArray[parseInt(horizontalVotes[voteIdx], 10)] = 1;
            } else {
                hVotesArray[arrayIdx] = parseInt(horizontalVotes[voteIdx], 10);
            }
            if (voteIdx + 1 >= horizontalVotes.length) {
                break;
            }
            if (!horizontalVotes[voteIdx + 1]) {
                break;
            }
            ++voteIdx;
            ++arrayIdx;
        }
        var retval = picType ?
                        new UCPPICHorizontalVote({
                            chlgname: ucpThread.challengeName(), 
                            poster: poster, 
                            voteText: horizontalVotes, 
                            votesArray: hVotesArray,
                            node: challengeEntry,
			    messageId: messageId,
                            ucpThread: ucpThread
                        }) :
                        new UCPHorizontalVote({
                            chlgname: ucpThread.challengeName(), 
                            poster: poster, 
                            voteText: horizontalVotes, 
                            votesArray: hVotesArray,
                            node: challengeEntry,
			    messageId: messageId,
                            ucpThread: ucpThread
                        });
        if (exampleVote) {
            retval.isExampleVote = function () {
                return true;
            };
        }
        return retval;
    }
    return new UCPVoteComment({
    	poster: poster,
    	node: challengeEntry,
    	messageId: messageId,
    	ucpThread: ucpThread
    });
}

function parseVerticalWeightedVote(data) {
    var ucpThread = data.ucpThread;
    var exampleVote = data.exampleVote;
    var poster = data.poster;
    var challengeEntry = data.challengeEntry;
    var messageId = data.messageId;
    var replytxt = data.replytxt;
    var challengeConfig = ucpThread.challengeDefinition();

    var vwVotesArray = [];
    var vwVoteLines = replytxt.split(/\n\s*/);
    // voteLines is an array:
    // #01: 3+1 (4)
    // #02: 3 (3)
    // ..
    // others use 1:
    var reVwVoteMatch1 = /(\d{1,2})#/;          // 01: -> no rating on this photo
    var reVwVoteMatch2 = /(\d{1,2})#(\d+)/;     // '01:1' --> simple rating on this photo
    var reVwVoteMatch3 = /(\d{1,2})#\d(\d|\s|\+)+\d+/; // sometimes, someone forgets the sum: 01:1+2
    var reVwVoteMatch4 = /(\d{1,2})#(\d|\s|\+|\(\d+\))*\((\d+)\)/; // '01:1+2(3)' && '01:1+2+3(4)' && '01:(2)' && '01:1+2(3)+2(5)'
    var vwVotesFound = 0;
    var vwVoided = false;
    for (var j = 0, vwVoteLinesLength = vwVoteLines.length; j < vwVoteLinesLength; ++j) {
        var vwVoteLine = vwVoteLines[j];
        // remove any leading spaces
        vwVoteLine = vwVoteLine.replace(/^\s*/, '');
        // ignore lines that are comment text
        if (vwVoteLine.match(/^\s*[a-zA-Z\s\.\,\(]{6}/)) { // 6? arbitrary
            continue;
        }
        // first seperate the photo number from the votes, in case it was seperated with a space
        var reVwReplaceMatch = /#(\d{1,2})/;
        vwVoteLine = vwVoteLine.replace(reVwReplaceMatch, "$1"); // remove leading #
        reVwReplaceMatch = /^(\d{1,2})([\-:+ ]+|\.{2,3})/;
        vwVoteLine = vwVoteLine.replace(reVwReplaceMatch, "$1#"); // insert # after photonumber
        // remove spaces
        vwVoteLine = vwVoteLine.replace(/\s/g, '');
        // remove any character before first vote in voteLine 00#-1+2
        vwVoteLine = vwVoteLine.replace(/(\d{1,2}#)[^0-9]*/, "$1"); // a remaining '-' would make it a negative vote :)
        if (vwVoteLine.match(/^\s*\d{1,2}\s*$/)) {
            vwVoteLine = vwVoteLine.replace(/^s*(\d{1,2})\s*$/, "$1#0"); // add a 0 as vote, where no rating is given
        }
        if (vwVoteLine.match(/void/i) && !vwVoteLine.match(/^\d/)) {
            // Skip replies where the vote has been voided
            vwVoided = true;
            break;
        }
        var photoNumber, photoScore;
        var verticalWVotes = reVwVoteMatch4.exec(vwVoteLine);
        if (verticalWVotes) {
            photoNumber = verticalWVotes[1];
            photoScore = verticalWVotes[3];
            vwVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(photoScore, 10);
            ++vwVotesFound;
            continue;
        }
        verticalWVotes = reVwVoteMatch3.exec(vwVoteLine);
        if (verticalWVotes) {
            photoNumber = verticalWVotes[1];
            reVwVoteMatch = /(\d{1,2})#([\d\s\+]+)/.exec(vwVoteLine);
            try {
                verticalWVotes = eval(reVwVoteMatch[2]);
                vwVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = verticalWVotes;
                ++vwVotesFound;
                continue;
            } catch (e) {
                // well, we tried, didn't we
            }
        }
        verticalWVotes = reVwVoteMatch2.exec(vwVoteLine);
        if (verticalWVotes) {
            photoNumber = verticalWVotes[1];
            photoScore = verticalWVotes[2];
            vwVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(photoScore, 10);
            ++vwVotesFound;
            continue;
        }
        verticalWVotes = reVwVoteMatch1.exec(vwVoteLine);
        if (verticalWVotes) {
            photoNumber = verticalWVotes[1];
            vwVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = 0;
            ++vwVotesFound;
            continue;
        }
        // thisvotes is still null => normal comment
    }       
    //GM_log(vwVotesArray);
    if (vwVoided) {
        var voidedWVote = new UCPVote({
            poster: poster, 
            voteText: vwVoteLines, 
            votesArray: vwVotesArray, 
            node: challengeEntry,
	    messageId: messageId,
            ucpThread: ucpThread
        });
        voidedWVote.isVoid = function () {
            return true;
        };
        return voidedWVote;
    }
    if (vwVotesFound <= 1) { // voting for one photo makes no sense
    	return new UCPVoteComment({
    		poster: poster,
    		node: challengeEntry,
    		messageId: messageId,
    		ucpThread: ucpThread
    	});
    }
    var vote = new UCPVerticalWeightedVote({
    	chlgname: challengeConfig.reName(), 
    	poster: poster, 
    	voteText: vwVoteLines, 
    	votesArray: vwVotesArray, 
    	node: challengeEntry,
    	messageId: messageId,
    	ucpThread: ucpThread
    });
    if (exampleVote) {
    	vote.isExampleVote = function () {
    		return true;
    	};
    }
    return vote;
}

function parseRatePhotoVote(data) {
    var ucpThread = data.ucpThread;
    var exampleVote = data.exampleVote;
    var poster = data.poster;
    var challengeEntry = data.challengeEntry;
    var messageId = data.messageId;
    var replytxt = data.replytxt;
    var challengeConfig = ucpThread.challengeDefinition();
    var rVotesArray = [];
    var rVoteLines = replytxt.split(/\n\s*/);
    // voteLines is an array (olho no lance):
    // photo 01: 7 pontos
    // photo 02: 4 pontos
    // ..
    // or (20 temas fotograficos)
    // 3 puntos: #12
    // 2 puntos: #2
    // ..
    // or (NA Nature)
    // 1°-#8
    // 2°-#1
    // 3°-#5
    var reVoteMatch1 = /(?:photo|foto|#)?\s*(\d+)[^\d]*(\d+)\s*p/i;
    var reVoteMatch2 = /(\d+)\s*p[^\d]*(\d+)/i;
    var reVoteMatch3 = /^#?(\d+)/;
    var reVoteMatch4 = /(\d+)\s*[,-]\s*(\d+)[,-]\s*(\d+)/;
    var reVoteMatch5 = /^(?:1|2|3)[^\d]+(\d+)/; // NA Nature
    var rVotesFound = 0;
    var weight = 3; // the top one is worth 3p
    rVoteLines.forEach(function (rVoteLine) {
        // remove any leading spaces
        rVoteLine = rVoteLine.replace(/^\s*/, '');
        if (rVoteLine.match(/void/i)) {
            // Skip replies where the vote has been voided
            return;
        }
        var photoNumber, photoScore;
        var rateVotes = reVoteMatch1.exec(rVoteLine);
        if (rateVotes) {
            photoNumber = rateVotes[1];
            photoScore = rateVotes[2];
            rVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(photoScore, 10);
            ++rVotesFound;
            return;
        }
        rateVotes = reVoteMatch2.exec(rVoteLine);
        if (rateVotes) {
            photoScore = rateVotes[1];
            photoNumber = rateVotes[2];
            rVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(photoScore, 10);
            ++rVotesFound;
            return;
        }
        rateVotes = reVoteMatch4.exec(rVoteLine);
        if (rateVotes) {
            rVotesArray[parseInt(rateVotes[1].replace(/^0/, ''), 10)] = challengeConfig.equalWeights() ? 2 : 3;
            rVotesArray[parseInt(rateVotes[2].replace(/^0/, ''), 10)] = 2;
            rVotesArray[parseInt(rateVotes[3].replace(/^0/, ''), 10)] = challengeConfig.equalWeights() ? 2 : 1;
            ++rVotesFound;
            return;
        }
	rateVotes = reVoteMatch5.exec(rVoteLine);
	if (rateVotes) {
	    photoNumber = rateVotes[1];
	    rVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = weight;
	    weight--;
	    ++rVotesFound;
	    return;
	}
        rateVotes = reVoteMatch3.exec(rVoteLine); // as last: matches above two
        if (rateVotes) {
            photoNumber = rateVotes[1];
            rVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = weight;
            ++rVotesFound;
            weight--;
            return;
        }
        // thisvotes is still null => normal comment
    });
    //my_log(vVotesArray);
    if (rVotesFound <= 0) {
        return new UCPVoteComment({
                    poster: poster, 
                    node: challengeEntry,
		    messageId: messageId,
                    ucpThread: ucpThread
                });
    }
    var vote = new UCPRatePhotoVote({
    	chlgname: challengeConfig.chlgname, 
    	poster: poster, 
    	voteText: rVoteLines, 
    	votesArray: rVotesArray, 
    	node: challengeEntry,
    	messageId: messageId,
    	ucpThread: ucpThread
    });
    if (exampleVote) {
    	vote.isExampleVote = function () {
    		return true;
    	};
    }
    return vote;
}

function parsePICVerticalVote(data) {
    var ucpThread = data.ucpThread;
    var exampleVote = data.exampleVote;
    var poster = data.poster;
    var challengeEntry = data.challengeEntry;
    var messageId = data.messageId;
    var replytxt = data.replytxt;
    var challengeConfig = ucpThread.challengeDefinition();
    var picX = data.picX;

    // scores are a single number, on separate lines
    var pvVotesArray = [];
    var pvVoteLines = replytxt.split(/\n\s*/);
    var pVotesFound = 0;
    // BUG: http://www.flickr.com/groups/the-storybook-challenge-group/discuss/72157625609019459/
    // 1. pic-v-3 with vote 2 3 1, is not an example vote
    // 2. the second vote is considered an example vote; should never happen
    pvVoteLines.each(function (pvVoteLine, k) {
        var pvVoteLine = pvVoteLines[k];
        // remove any leading spaces
        pvVoteLine = pvVoteLine.replace(/^\s*/, '').replace(/#\s+(\d+)/, "$1");
        pvVoteLine = pvVoteLine.replace(/^foto[^\d]*|^photo[^\d]*/i, '');
        pvVoteLine = pvVoteLine.replace(/^n°[^\d]*/i, '');
        // ignore lines that are comment text
        if (pvVoteLine.match(/^\s*[a-zA-Z\s\.\,\(]{6}/)) { // 6? arbitrary
            return;
        }
        var reReplaceMatch = /#(\d{1,2})/;
        pvVoteLine = pvVoteLine.replace(reReplaceMatch, "$1"); // remove leading #
        var picVoteMatch = /^\s*(\d+)/.exec(pvVoteLine);
        if (picVoteMatch) {
            var voteIdx = parseInt(picVoteMatch[1].replace(/^0(\d+)/, '$1'), 10);
            if (voteIdx < 256) { // if someone enters a 'summary' of the scores (2302042141) => oom
                pvVotesArray[voteIdx] = 1;
                ++pVotesFound;
                return;
            }
        }
    });
    if (picX === undefined || pVotesFound <= 0) {
        return new UCPVoteComment({
                       poster: poster, 
                       node: challengeEntry,
		       messageId: messageId,
                       ucpThread: ucpThread
                   });
    }
    //else { // picX defined
    if (pVotesFound < picX) {
    	return new UCPVoteComment({
    		poster: poster,
    		node: challengeEntry,
    		messageId: messageId,
    		ucpThread: ucpThread
    	});
    }
    var picVVote = new UCPPICVerticalVote({
    	chlgname: challengeConfig.reName(), 
    	poster: poster, 
    	voteText: pvVoteLines, 
    	votesArray: pvVotesArray, 
    	node: challengeEntry,
    	messageId: messageId,
    	ucpThread: ucpThread
    });
    // also catches rules:
    // 1) ...
    // 2) ...
    // so, try to remove these
    /*
            if (!exampleVote) {
                var picExampleVote = picX > 1; // assume an example vote, only if voting for multiple
                                              // voting for photo 1 results in example vote otherwise
                GM_log("checking for example vote: ");
                pvVotesArray.each(function (vote, idx) {
                // TODO: check
                GM_log("idx: " + idx + " - vote: " + vote);
                    if (idx == 0) return; // skip index 0: 1-based voting
                    if (picExampleVote && (!vote || isNaN(vote))) {
                        picExampleVote = false;
                        return;
                    }
                });
            }*/
    if (/*picExampleVote ||*/ exampleVote) {
    	picVVote.isExampleVote = function () {
    		return true;
    	}; // override default behaviour
    }
    return picVVote;
}


// The challenger or competitor's photo in  one of the challenges.
var UCPCompetitor = new Class({
    //Implements: [Options],
    options: {
        node: null,
	messageId: null,
        photo: null,
        poster: null,
        owner: null,
        photoId: null,
	photoTitle: null,
	photoNumber: null,
        comments: [],
	approved: false,
	approvedBy: null,
        ucpThread: null
    },
    initialize: function (options) {
        //this.setOptions(options); => hangs the Kanchenjunga challenge (cause: ucpThread)
	for (var key in options) {
		this.options[key] = options[key];
	}
    },
    ucpThread: function () {
        return this.options.ucpThread;
    },
    toString: function () {
        return this.options.poster.username;
    },
    // accessors
    node: function () {
        return this.options.node;
    },
    photo: function () {
        return this.options.photo;
    },
    poster: function () {
        return this.options.poster;
    },
    owner: function () {
        return this.options.owner;
    },
    photoId: function () {
        return this.options.photoId;
    },
    photoTitle: function () {
        return this.options.photoTitle;
    },
    photoNumber: function () {
    	return this.options.photoNumber;
    },
    approved: function () {
        return this.options.approved;
    },
    approvedBy: function () {
        return this.options.approvedBy;
    },
    hasError: function () {
        return this.options.comments.some( function (msg) {
            return msg.type == 'error';
        });
    },
    hasWarning: function () {
        return this.options.comments.some( function (msg) {
            return msg.type == 'warning';
        });
    },
    error: function () {
        var retval = '';
        this.options.comments.each(function (comment) {
            if (comment.type === 'error') {
                if (retval.length > 0) retval += " - ";
                retval += comment.msg;
            }
        });
        return retval;
    }, 
    warning: function () {
        var retval = '';
        this.options.comments.each(function (comment) {
            if (comment.type === 'warning') {
                if (retval.length > 0) retval += " - ";
                retval += comment.msg;
            }
        });
        return retval;
    }, 
    printStatus: function () {
        if (this.options.ucpThread.options.needsStatusOnChallengePage) {
		if ($chk(this.options.messageId)) { // the challenge announcement does not have an entry_id
			var pageAnchor = $$('td.Who a[name=comment' + this.options.messageId + ']')[0].getParent('tr').getElements('small').getLast();
		} else {
			if (!global_group_preferences.groupConfig().allowsPhotoInAnnouncement()) {
				GM_log("ERROR: found a photo entry without message id; skipping");
				return;
			}
			pageAnchor = $$('td.Who')[0].getParent('tr').getElements('small').getLast();
		}
		    if ($chk(pageAnchor) && $chk(pageAnchor.getParent('div.ucpdiv'))) {
			pageAnchor = pageAnchor.getParent('div.ucpdiv');
		    }
		    if (!$chk(pageAnchor)) {
			pageAnchor = this.node().getParent('td.Said').getElements('small').getLast();
		    }
		    if (!$chk(pageAnchor)) {
			pageAnchor = this.options.node;
		    }
		    var message = "UCheckPlayNG: found a photo posted by <b>" + this.poster().username + "</b>";
		    var commentAnchor = new Element('small', {
			html: message
		    }).inject(new Element('div', {
			'class': 'ucpdiv'
		    }).inject(pageAnchor, 'after'));
		    this.options.comments.each(function (comment) {
			commentAnchor.adopt(
			    new Element('span', {
				html: " - " + comment.msg,
				styles: {
				    color: comment.type === 'comment' ? '':
					   comment.type === 'warning' ? 'orange' :
					   this.poster().admin ? 'orange' : 'red'
				}
			    })
			);
		    }, this);
        }
    },
    addError: function (error) {
        this.options.comments.include({ msg: error, type: 'error' });
    },
    addWarning: function (warning) {
        this.options.comments.include({ msg: warning, type: 'warning' });
    },
    addComment: function (comment) {
        this.options.comments.include({ msg: comment, type: 'comment' });
    }
});

function parsePICPlayerVote(data) {
    var ucpThread = data.ucpThread;
    var exampleVote = data.exampleVote;
    var poster = data.poster;
    var challengeEntry = data.challengeEntry;
    var messageId = data.messageId;
    var replytxt = data.replytxt;
    var photoArray = data.photoArray;
    var challengeConfig = ucpThread.challengeDefinition();
    var picX = data.picX;

    // used in MatchPoint (http://www.flickr.com/groups/matchpoint/discuss/)
    // and in F64 Challenge (http://www.flickr.com/groups/flickr64challenge/discuss/)
    var ppVoteLines = replytxt.split(/\n\s*/);
    var player;
    var ppVote = undefined;
    var ppVotesFound = 0;
    var ppVotesArray = []; //[challengeConfig.options.neededPhotos + 1];
    ppVoteLines.each(function (ppVoteLine, l) {
	if (ppVoteLine.length < 3) {
		return;
	}
    	// TODO: add new variable to challengeDefinition: voteWithBuddyIcon
        // (remove buddyicons first: contains @)
        ppVoteLine = ppVoteLine.replace(/<img[^>]+>/g, '');
        // ignore lines that are comment text
        if (!ppVoteLine.match(/@|#|p(oi)?nt/i) && document.location.href.match('matchpoint')) {
            return;
        }
        // remove any leading spaces
        ppVoteLine = ppVoteLine.replace(/^\s*/, '');
        // remove html, which does not fit in a username
        ppVoteLine = ppVoteLine.replace(/[@#](\s*[^<]*)/, '@ $1');
	// MatchPoint
        var picPVoteMatch = /\d+[^\d]*[@#]\s*([^<]*)/.exec(ppVoteLine);
        if (!picPVoteMatch) {
            picPVoteMatch = ppVoteLine.match(/(?:point|pnt)[^\d]*\d+[^@#]*[@#]\s*([^<]*)/i);
        }
	// F64
	if (!picPVoteMatch) {
		picPVoteMatch = ppVoteLine.match(/vote:?\s*(.*)/i);
	}
	if (!picPVoteMatch) {
		// sometimes, only the name is given
		picPVoteMatch = ppVoteLine.match(/(.*)/);
	}
        if (picPVoteMatch) {
	    player = picPVoteMatch[1];
            // cleanup player
            player = player.replace(/\)/g, ''); // smiley's !!
            player = player.replace(/<[^>]*>/g, '').replace(/\s*$/, '');
            player = player.replace(/&nbsp;/g, ' ');
            // remove insignificant characters at the end of the player entry
            player = player.replace(/(?:\.|\!|\s)+$/, '');
            // compensate for spaces, and underscores in usernames, or the lack of them
            player = player.replace(/(?:_|\s|\.|\)|\()+/g, '');
            // people tend to mistype usernames with numbers
            if (player.replace(/\d/g, '').length > 0 ) {
                player = player.replace(/\d/g, '');
            }
            photoArray.each(function (photo, ppPhotoIdx) {
                var photo = photoArray[ppPhotoIdx];
                if (!(photo instanceof UCPCompetitor)) {
                    return;
                }
                var challenger = photo.poster().username.replace(/(?:_|\s|\)|\(|\.)+/g, '');
                if (challenger.replace(/\d/g, '').length > 0) {
                    challenger = challenger.replace(/\d/g, '');
                }
                // voters make shortcuts for usernames, and don't care for case
                // they also misspell Mustela, and anglerove
                if (photo && (
                        (challenger.match(/Mustela.Nivalis/) && player.match(/Must(e|a)l?a/i)) ||
                        (challenger.match(/anglerove/) && player.match(/angelrove/i))
                    )) {
                    ppVotesArray[ppPhotoIdx + 1] = 1; //parseInt(score, 10);
			++ppVotesFound;
                    return;
                }
		try {
			if (photo && challenger.toLowerCase().match(player.toLowerCase())) {
			    ppVotesArray[ppPhotoIdx + 1] = 1; // parseInt(score, 10);
				++ppVotesFound;
			}
		} catch (e) {
		    GM_log("error: " + e);
			GM_log("challenger='" + challenger + "' - player='" + player + "'");
		}
            });
        }
    });
    if (ppVotesFound == 0) {
        ppVote = new UCPVoteComment({
                        poster: poster,
                        node: challengeEntry,
			messageId: messageId,
                        ucpThread: ucpThread
                    });
    } else {
	ppVote = new UCPPICPlayerVote({
		    chlgname: challengeConfig.reName(), 
		    poster: poster, 
		    content: replytxt,
		    voteText: ppVoteLines, 
		    votesArray: ppVotesArray, 
		    node: challengeEntry,
		    messageId: messageId,
		    ucpThread: ucpThread
		});
	    if (exampleVote) {
		ppVote.isExampleVote = function () {
			return true;
		}; // override default behaviour
	    } else {
		    if (picX != undefined && picX < ppVotesFound) {
			ppVote.addError("voted for too many players");
		    }
		    if (picX != undefined && picX > ppVotesFound) {
			ppVote.addError("did not vote for " + picX + " players (" + ppVotesFound + ")");
		    }
	    }
    }
    return ppVote;
}

var global_user_nsid= GM_getGlobalNsid();
var global_group_adminOrMod = false;

function ucpCreateCompetitor(data) {
	var messageId = data.messageId;
	var photoNode = data.node;
	var poster = data.poster;
	var photoNumber = data.number;
	var ucpThread = data.ucpThread;
	var _debug = data.debug;

    var ownerId;
    var photoId = extractPhotoId(photoNode.get('src'));
    var photoTitle = $chk(photoNode.get('alt')) ? photoNode.get('alt') : photoNode.get('title');
    if (photoNode.getParent('a') && photoNode.getParent('a').get('href')) {
        ownerId = photoNode.getParent('a').get('href').split('/')[4];
        //GM_log("ownerId: " + ownerId);
        var owner;
        if (ownerId === poster.userid) {
            owner = poster;
        } else {
            owner = { username: "", userid: ownerId };
        }
	var topNode = photoNode;
	while ($chk(topNode.getParent('div'))) {
		topNode = topNode.getParent('div');
	}
        var approvalImg = topNode.getElement('img[alt*="UCPAapproved"]');
        if (approvalImg && ucpThread.topic()) {
            var approvalCheck = ucpCheckPhotoApproval(photoNode, ucpThread.topic());
            // returns
            // {
            //      approved: true/false,
            //      approver: string,
            //      version: number,
            //      checksum: true/false,
            //      photoChecksum: true/false,
            //      error: string
            //      photoId: string
            //  }
            var comment;
            if (approvalCheck.approved) {
                comment = { msg: " approved by " + approvalCheck.approver, type: 'comment' };
                return new UCPCompetitor({
                    node: topNode,
		    messageId: messageId,
                    photo: photoNode,
                    photoId: photoId,
		    photoTitle: photoTitle,
		    photoNumber: photoNumber,
                    comments: [ comment ],
                    poster: poster,
                    owner: owner,
		    approved: true,
		    approvedBy: approvalCheck.approver,
                    ucpThread: ucpThread
                });
            } else if (approvalCheck.ignored) {
                comment = { msg: "non competing image (by " + approvalCheck.approver + ")", type: 'comment' };
                return new UCPVoteComment({
                    node: topNode,
		    messageId: messageId,
                    poster: poster,
                    comments: [ comment ],
                    ucpThread: ucpThread
                });
            } else {
                var competitor = new UCPCompetitor({
                    node: topNode,
		    messageId: messageId,
                    photo: photoNode,
                    photoId: photoId,
		    photoTitle: photoTitle,
		    photoNumber: photoNumber,
                    poster: poster,
                    owner: owner,
		    approved: true,
                    approvedBy: approvalCheck.approver,
                    ucpThread: ucpThread
                });
		if (global_group_adminOrMod) {
			competitor.addError(approvalCheck.error);
			ucpThread.addVotingError(approvalCheck.error);
		}
                return competitor;
            }
        }
        var comp = new UCPCompetitor({
        	node: topNode,
        	messageId: messageId,
        	photo: photoNode,
        	photoId: photoId,
        	photoTitle: photoTitle,
        	photoNumber: photoNumber,
        	poster: poster,
        	owner: owner,
        	ucpThread: ucpThread,
        	debug: _debug
        });
        return comp;
    }
    comment = { msg: "photo has no link to photo page", type: 'error' };
    var competitor = new UCPCompetitor({
    	node: topNode,
    	messageId: messageId,
    	photo: photoNode,
    	photoId: photoId,
    	photoTitle: photoTitle,
    	photoNumber: photoNumber,
    	poster: poster,
    	comments: [ comment ],
    	ucpThread: ucpThread
    });
    if (global_group_adminOrMod) {
    	competitor.addError(comment);
    	ucpThread.addVotingError(comment.msg);
    }
    return competitor;
}

// end former common library

function updateClock(callback) {
	var lastchecked = GM_getValue(UCPprefix + ".clock.lastcheck");
	var interval = 2 * 60 * 60; // only refresh every 2 hours
	var now = parseInt(new Date().getTime() / 1000); // API returns time in seconds
	if ($chk(lastchecked) && (parseInt(lastchecked) + parseInt(interval) > now)) {
		callback({ success: true });
		return;
	}
	readFlickrClock(callback);
}

function readFlickrClock(callback) {
	// get the latest upload using the API
	// use the Flickr API to get its correct upload time
	new Request({
		onSuccess: function (responseText, responseXML) {
			var result = JSON.parse(responseText);
			if (result.stat != 'ok') {
				callback({ success: false, message: "ERROR reading photo info: " + result.message });
				return;
			}
			var lastupdate = result.photos.photo.sort(function(photoA, photoB) {
				return parseInt(photoB.dateupload) - parseInt(photoA.dateupload); // sort latest first
			})[0].dateupload;
			var stored = GM_getValue(UCPprefix + ".clock.lastupdate");
			if (!$chk(stored) || (parseInt(stored) !== parseInt(lastupdate))) {
				var now = parseInt(new Date().getTime() / 1000); // API returns time in seconds
				GM_setValue(UCPprefix + ".clock.lastupdate", lastupdate);
				GM_setValue(UCPprefix + ".clock.lastcheck", now);
			}
			callback({ success: true });
		},
		onFailure: function (response) {
			callback({ success: false, message: "ERROR reading recent photos: " + response.statusText });
		}
	}).get('/services/rest', {
		api_key: GM_getPublicKey(),
		auth_hash: GM_getAuthHash(),
		auth_token: GM_getAuthToken(),
		format: 'json',
		nojsoncallback: 1,
		method: 'flickr.photos.getRecent',
		extras: 'date_upload',
		per_page: 10,
		page: 1
	});
}

function calculateFlickrClock() {
    	var flickrClockSeconds = GM_getValue(UCPprefix + ".clock.lastupdate");
	if ($chk(flickrClockSeconds)) {
		var oldFlickrClock = new Date(parseInt(flickrClockSeconds) * 1000);
		var flickrClockLastCheck = new Date(parseInt(GM_getValue(UCPprefix + ".clock.lastcheck")) * 1000);
		if (flickrClockLastCheck.toString().match(/invalid/i)) {
			GM_deleteValue(UCPprefix + ".clock.lastcheck");
			GM_deleteValue(UCPprefix + ".clock.lastupdate");
			return undefined;
		}
		var now = new Date();
		var timeElapsed = now - flickrClockLastCheck;
		var estimatedFlickrClock = new Date(oldFlickrClock.getTime() + timeElapsed);
		//GM_log("DEBUG: storedClock='" + oldFlickrClock.toUTCString() + "' - lastCheck='" + flickrClockLastCheck.toUTCString() + "' - elapsed='" + timeElapsed + "' - estimated='" + estimatedFlickrClock.toUTCString() + "'");
		return estimatedFlickrClock;
	}
	return undefined;
}

// defaults
var username = GM_getLoggedInUser();
//var username = "Little_Debbie"; DEBUG
var playernumber = 0; // keep it global! passing it along as a member of groupConfig seems not to work
var global_ucp_language = null;
var global_groupname = null;
            var reGroupnameMatch = /.*flickr.com\/groups\/([^\/]*)\//;
            if (document.location.href.match(reGroupnameMatch)) {
                global_groupname = reGroupnameMatch.exec(document.location.href)[1]; // needed for version check
            }
var debug = false;

var ucpGroupConfigReader = new UCPGroupConfigReader();
var ucpLanguageConfigReader = null;
var topicListingTable = null;
    
function initialize(callback) {
    if (!$chk(username) || username === "") {
        GM_log("Sign in to your Flickr account if you want to take part in challenges in this group");
	callback({ success: false, message: "not logged in" });
        return;
    }

    try {
        topicListingTable = document.getElement('ul.topic-list');
    } catch (e) {
    }

	var FSM = new Element('div', { 
		id: 'UCP_FSM',
		'class': 'UCP_hidden_from_view'
	}).inject($$('body')[0]);
	FSM.adopt( new Element('div', { html: "UCheckPlay progress: " }));
	FSM.adopt(
		new Element('div', {
			id: 'UCP_FSM_groupconfig',
			events: {
				'groupconfig': function (evt) {
					$('UCP_FSM_groupconfig_img').set('src', images.updating);
					$('UCP_FSM_groupconfig_img').set('title', 'updating');
				    try {
					var group_name = reGroupnameMatch.exec(document.location.href)[1];
					ucpGroupConfigReader.createGroupConfig(group_name, function (retval) {
						$('UCP_FSM_groupconfig_img').set('title', 'done');
						if (!retval.success) {
							$('UCP_FSM_groupconfig_img').set('src', images.error);
							$('UCP_FSM_groupconfig_txt').set('html', "error creating group config: " + retval.message);
							callback({ success: false, message: "error creating group config: " + retval.message });
							return;
						}
						$('UCP_FSM_groupconfig_img').set('src', images.defaultCheck);
						global_group_config = retval.groupConfig;
						if (!$chk(global_group_config)) {
							GM_log("unsupported group; aborting");
							$('UCP_FSM_groupconfig_img').set('src', images.error);
							$('UCP_FSM_groupconfig_txt').set('html', "unsupported group");
							callback({ success: false, message: "unsupported group" });
							return;
						}
    						global_group_preferences = new UCPGroupPreferences({groupConfig: global_group_config});
						$('UCP_FSM_flickrclock').fireEvent('calculateclock');
					});
				    } catch (e) {
					GM_log("unsupported group; aborting (" + e + ")");
					$('UCP_FSM_groupconfig_img').set('src', images.error);
					$('UCP_FSM_groupconfig_img').set('title', 'error');
					$('UCP_FSM_groupconfig_txt').set('html', "unsupported group (" + e + ")");
					callback({ success: false, message: "unsupported group (" + e + ")" });
					return;
				    }
				}
			}
		}).adopt(
			new Element('img', { id: 'UCP_FSM_groupconfig_img', src: images.queued, title: 'queued' }),
			new Element('span',{ id: 'UCP_FSM_groupconfig_txt', html: 'read group configuration' })
		),
		new Element('div', {
			id: 'UCP_FSM_flickrclock',
			events: {
				'calculateclock': function (evt) {
					$('UCP_FSM_flickrclock_img').set('src', images.updating);
					$('UCP_FSM_flickrclock_img').set('title', 'updating');
					if (global_group_config.usesTimeConstraints() || global_group_config.hasTimeConstrainedChallenges()) {
					    try {
						updateClock(function (retval) {
							$('UCP_FSM_flickrclock_img').set('src', images.defaultCheck);
							$('UCP_FSM_flickrclock_img').set('title', 'done');
							if (!retval.success) {
								GM_log("failed reading the Flickr clock: " + retval.message);
								// ignore this error, and continue
								$('UCP_FSM_flickrclock_img').set('src', images.error);
								$('UCP_FSM_flickrclock_img').set('title', 'error');
								$('UCP_FSM_flickrclock_txt').set('html', "failed reading the Flickr clock: " + retval.messag);
							}
							GM_log("DEBUG: Flickr clock is '" + calculateFlickrClock() + "'");
							$('UCP_FSM_language').fireEvent('readlanguage');
						});
					    } catch (e) {
						GM_log("Exception: " + e);
						$('UCP_FSM_flickrclock_img').set('src', images.error);
						$('UCP_FSM_flickrclock_img').set('title', 'done');
						$('UCP_FSM_language').fireEvent('readlanguage');
					    }
					} else {
						$('UCP_FSM_flickrclock_img').set('src', images.defaultCheck);
						$('UCP_FSM_flickrclock_img').set('title', 'done');
						$('UCP_FSM_language').fireEvent('readlanguage');
					}
				}
			}
		}).adopt(
			new Element('img', { id: 'UCP_FSM_flickrclock_img', src: images.queued, title: 'queued' }),
			new Element('span',{ id: 'UCP_FSM_flickrclock_txt', html: 'read Flickr clock' })
		),
		new Element('div', {
			id: 'UCP_FSM_language',
			events: {
				'readlanguage': function (evt) {
					$('UCP_FSM_language_img').set('src', images.updating);
					$('UCP_FSM_language_img').set('title', 'updating');
					ucpLanguageConfigReader = new UCPLanguageConfigReader();
					ucpLanguageConfigReader.createLanguage({ languagename: global_group_preferences.language(),
						legacyLabels: global_group_config.hasLegacyLabels() && (global_group_preferences.useLegacyLabels() || global_group_config.mandatoryGroupLabels()) ? 
						    global_group_config.legacyLabels() : undefined,
						languageOverrides: global_group_config.languageOverrides(),
						callback: function(retval) {
							global_ucp_language = retval.language;
							$('UCP_FSM_language_img').set('src', retval.success ? images.defaultCheck : images.error);
							$('UCP_FSM_language_img').set('title', retval.success ? 'done' : retval.message);
							$('UCP_FSM_adminormod').fireEvent('adminormod');
						}
					});
    					ucpLanguageConfigReader.checkForUpdates(global_group_preferences.language(), function () {});
				}
			}
		}).adopt(
			new Element('img', { id: 'UCP_FSM_language_img', src: images.queued, title: 'queued' }),
			new Element('span',{ id: 'UCP_FSM_language_txt', html: 'read language' })
		),
		new Element('div', {
			id: 'UCP_FSM_adminormod',
			events: {
				'adminormod': function (evt) {
					$('UCP_FSM_adminormod_img').set('src', images.updating);
					$('UCP_FSM_adminormod_img').set('title', 'updating');
					fetchGroupAdministratorOrModerator(global_user_nsid, function (retval) {
						if (!retval.success) {
							$('UCP_FSM_adminormod_img').set('src', images.error);
							$('UCP_FSM_adminormod_img').set('title', 'error');
							GM_log("error fetching member status: " + retval.message);
						} else {
							$('UCP_FSM_adminormod_img').set('src', images.defaultCheck);
							$('UCP_FSM_adminormod_img').set('title', 'done');
							global_group_adminOrMod = retval.adminOrMod;
						}
						callback({ success: true }); // run as a non-administrator, then
					});
				}
			}
		}).adopt(
			new Element('img', { id: 'UCP_FSM_adminormod_img', src: images.queued, title: 'queued' }),
			new Element('span',{ id: 'UCP_FSM_adminormod_txt', html: 'check for admod rights' })
		)
	);
    
	$('UCP_FSM_groupconfig').fireEvent('groupconfig');

}

    // if you copy any of this source, please use your own flick api_key
    // you can easily generate one on http://www.flickr.com/services/apps/create/

var images = {
	download: 'data:image/png;base64,' +
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAANkE3LLaAgAAAmhJREFU' +
	    'eJyVkk1IVFEYhp9zz517xxnHf8RGG0H8GYYKCiEiCMWkItq2diUE5aZFf8sWEVQE4qZFyxZCbVpE' +
	    'RIug3JiKhKEzlalkpkwzNnPnXufeuaeFY/lDRc/yPd/78PFxBDsJxgZiD+OJeKfruptJJU0AUtdZ' +
	    'WPy8nLqdOgVktgr6LkGop7en/9LZiw15J7+Z+OUHM8To+GjT3fv36tn4swDXKzjfC6tYjrUjd/wK' +
	    'nFJ+A1Db870CYbPupbE9C0Q5VFDUKnCxdo/vEXietMmX0tjK/t1X4JYKuJoFAm+3QAJhAEzqPJkT' +
	    'edIUcbYvgIdJUeYEJrU4ZNjczxLmUQYPX+i4WaWHhJK+aItFa/fXxaTCQysbfAUoybfcipqbX8xo' +
	    'rizlsZl+lBzWNZP2cNRpTDQeJBww8SlisYqmgyZUWSDwPUFVZUh0H0jUpe3vfFifQZh+l27PMjb3' +
	    'Zsk1etcDLbUJQgETXYeAriM0HZTC9Ys4yiFTzLJirWHbaZanwJrklcRiNv+Rr2bLRl9VW84oGFnQ' +
	    'PAxhIIXEVx6Wn2XNm2fVXyKoFcm+U15qhOveJ0YkgPrBZGaGLzUdpf7W9hrDkNVEtBoispagFkKK' +
	    'AELTqDEM0tOOO37Lu7qR5M7mZcr4OaZXpvxUQ9w/3R0/YjbqnTTozVTLBkwtTHUwQGoy6b64lhty' +
	    'Ugxv9X4JAEo53i9MFJOReLrvWOJ4KCoPEZH1BIMur98+LTy5vDJUSPJgz2/ajRHl3OCz5rUZ/7FK' +
	    '+s/VlbFEPtLFwD+L2zGjnBx6uW/1xkSbVdXJ+f8qbxFs4kSolTN/m/kJ0mH+2AHDp/kAAAAASUVO' +
	    'RK5CYII=',
	queued: 'data:image/png;base64,' +
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A' +
	    '/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wECgw2KIam8LsAAAK9SURBVDjL' +
	    'fZJZSFRxFMZ//ztz73XSbNCZQrMdpcDICluQJIiiosJ2iOohKoqQioiKKDIKiuhFK+qhjQZNpcVB' +
	    'bmn0UKEt1GhGCzGWmblMmdR1dMbR+fcgI2rW93Q43wLn4wBQlDmXCKSUfXOzu2RHs7tk11DcjfQ0' +
	    'AKwA6x4/pfGeMcMixHIhRI6UEiEEnbXew1KGAc5Hdt/Ly46HkLcSFy1+DaBEEhuKXIVKKHTs04W8' +
	    'IiEEAN3mbxk2TQCEEHw5l2vQ4T/y9abLFfH1BRgF+QXfSorRo2PWfDxzshCgx2/KcHtvwKcjhwxV' +
	    'ysVN7tuUFbhuMRj7otD22kRe9fbN0nevVO6HuEfpKV8q0lPqT0RbHa3FhbJmy0Z50MalA1FoEZ8Y' +
	    'HLRH5YRTJ80bYLXTwmwFLAHBs0QLxs8QnlNd7OuvF97dO3P1OOdKqVqk0DUC5s+Hc46d2doKPf2F' +
	    'GWC5sn/32egR8avCgU6CbhfB9rYH1uY31RmO0eOS0DWEquLzvl8x2AxQAT3fnlcuS5yUMibc0oh0' +
	    'JtCm2jJFjk62TZIlQSKhXVfuZ1e9uzEyeXJL/4C2z95Rp1OTV9hDci0gEChBeDCgA99Mu27GTvZp' +
	    'abNiy/IvDs966BEQxlg+XyzYvONXsMZjjmipdsZXNgf/KvHp1IQYZ9KUVotjpPbZU8nqt/UxL7KW' +
	    '1gBi4V1j2pW0Sb+TszbQ9bamy+d9FT+3qqF9wB/EZSwptyYkqR2vK8JGbf2mNvDLseM1OXa8Wgem' +
	    '8a52i7+sWKrjJqr2WYvcEZ81MtSVlwYm6BrX6xo2RSvkAwybl9lL5l5gmIVrl6s+yG2//FfrOwPd' +
	    'f52QreGwKiTEKrzJ6ejd/fC8/IpQcEyfMQbgqA1hSlK7wzTldfGjr7zDNobEbYf2+I5TfzIU9y/P' +
	    'AKwX2NcJ7P/T/AEtxhn4rk6s8QAAAABJRU5ErkJggg==',
	updating: chrome.extension.getURL('images/pulser2.gif'),
	defaultCheck: chrome.extension.getURL('images/icon_check.png'),
	error: chrome.extension.getURL('images/icon_error_x_small.png'),
	paypal: chrome.extension.getURL('images/paypal.png'),
        amazon: chrome.extension.getURL('images/amazon.png'),
        bitcoin: chrome.extension.getURL('images/bitcoin.png')
    };

    function toggleGroupList() {
        var groupListDialog = $("UCPNGSupportedGroupListDiv");
        if (groupListDialog) {
            groupListDialog.dispose();
        } else {
            var table = new Element('table', {
                styles: {
                    border: '0',
                    cellPadding: '5',
                    cellSpacing: '0'
                }
            });
            var rowIndex = 0;
            var row = null;
            ucpGroupConfigReader.groupList(function(retval) {
		if (!retval.success) {
			new Element('div', {
				html: retval.message
			}).inject(groupListDialog);
			return;
		}
		var groupList = retval.groupList;
		var groupArray = [];
		$each(groupList, function(group, it_groupname) {
			if (!$chk(group.hideFromSupportedGroups) || (group.hideFromSupportedGroups !== true && group.hideFromSupportedGroups !== 'true')) {
				group.groupname = it_groupname;
				groupArray.include(group);
			}
		});
		var nonActive = false;
		groupArray.sort(function(groupA, groupB) {
			if ($chk(groupA.activity) && $chk(groupB.activity)) {
				return 0;
			}
			if ($chk(groupA.activity)) {
				return 1;
			}
			if ($chk(groupB.activity)) {
				return -1;
			}
			return groupB.name.replace(/^[\*\s\¡:]*/g, '').toLowerCase() > groupA.name.replace(/^[\*\s\¡:]*/g, '').toLowerCase() ? -1 : 1;
		}).each(function (group) {
			if (!nonActive && $chk(group.activity)) { // starting with the abandoned and deleted ones
				nonActive = true;
				rowIndex = 0;
				row = new Element('tr').inject(table);
				var cell = new Element('th', { 
					colspan: 3,
					events: {
						click: function () {
							var nonActiveRows = $$('tr.UCP_non_active_row');
							var hidden = nonActiveRows.some(function (row) {
								return row.hasClass('UCP_hidden_from_view');
							});
							if (hidden) {
								nonActiveRows.removeClass('UCP_hidden_from_view');
							} else {
								nonActiveRows.addClass('UCP_hidden_from_view');
							}
						}
					}
				}).inject(row);
				cell.adopt(
					new Element('img', { src: images.download, styles: { 'max-height': '10px' } }),
					new Element('span', { html: '(temporarily) abandonded and deleted groups', styles: { 'margin-left': '12px' } })
				);
			}
			if (rowIndex % 3 === 0) {
				row = new Element('tr', { 'class': nonActive ? 'UCP_hidden_from_view UCP_non_active_row' : ''} ).inject(table);
			} else {
				new Element('td', {
				    html: "&nbsp;",
				    styles: {
					borderRight: 'solid grey 1px'
				    }
				}).inject(row);
			}
			rowIndex++;
			new Element('a', {
				href: "http://www.flickr.com/groups/" + group.groupname + "/discuss/",
				html: group.name,
				title: $chk(group.activity) ? group.activity : group.name,
				styles: {
					'text-decoration': group.activity == 'abandoned' || group.activity == 'deleted' ? 'line-through' : 'none',
					color: group.activity == 'deleted' ? 'brown' : ''
				}
			}).inject(new Element('td').inject(row));
		    });
	    });

            new Element('button', {
                type: 'submit',
                html: 'Close',
                'class': 'CancelButt',
                events: {
                    click: toggleGroupList
                }
            }).inject(new Element('td', {
                colSpan: '8',
                align: 'right'
            }).inject(new Element('tr').inject(table)));

            var dialogDiv = new Element('div', {
                'id': "UCPNGSupportedGroupListDiv",
                styles: ucpDialogStyle
            }).adopt(table);
            $('UCheckPlayNGPreferences').getParent().adopt(dialogDiv);
        }
    }

    function togglePreferencesDialog() {
        var configDialog = $("UCPNGConfigDialogDiv");
        if (configDialog) {
            configDialog.destroy();
        } else {
            new Element('div', {
                id: "UCPNGConfigDialogDiv",
                styles: ucpDialogStyle
            }).inject($("UCheckPlayNGPreferences").getParent()).adopt(
            table = new Element('table', {
                border: '0',
                cellPadding: '5',
                cellSpacing: '0'
            }));
            // title
	    var headerRow = new Element('tr').inject(table);
	    var donationCell = new Element('td').inject(headerRow);
            headerRow.adopt(header = new Element('td', { colspan: '2', width: '100%' }));
		new Element('span', {
                        title: 'consider a donation',
                        html: 'donate:',
                        styles: {
                                'font-style': 'italic',
                                'font-size': 'smaller',
                                'vertical-align': 'middle'
                        }
                }).adopt(
                        new Element('a', {
                                href: 'https://www.paypal.com/be/cgi-bin/webscr?cmd=_send-money&email=alesadam@ymail.com&amount_ccode=USD&payment_type=S',
                                title: 'donate with Paypal',
                                target: '_blank'
                        }).adopt(
                                new Element('img', { src: images.paypal, styles: { 'vertical-align': 'middle' } })
                        ),
                        new Element('a', {
                                href: 'http://www.amazon.co.uk/registry/wishlist/RWKOXV8NS09I/',
                                title: 'buy me something from my Amazon wishlist (UK)',
                                target: '_blank'
                        }).adopt(
                                new Element('img', { src: images.amazon, styles: { 'vertical-align': 'middle' } })
                        ),
                        new Element('a', {
                                href: 'http://www.amazon.com/gp/wishlist/2FWKY0A7NVSOJ/',
                                title: 'buy me something from my Amazon wishlist (US)',
                                target: '_blank'
                        }).adopt(
                                new Element('img', { src: images.amazon, styles: { 'vertical-align': 'middle' } })
                        ),
                        new Element('a', {
                                href: 'javascript:void(O);',
                                title: 'donate Bitcoins',
                                events: {
                                        click: function (evt) {
                                                // based on http://leo.bogert.de/2012/02/22/simple-bitcoin-donate-button/
                                                window.prompt ('Please copy-paste my Bitcoin address to your Bitcoin software - I cannot do it automatically.\nTo copy it, right-click the selected text and select \'Copy\'.\nThen right-click the address field in your Bitcoin software and select \'Paste\'.', '19DvtjDgiSx9991xVkXSYSovsSoreSGDC7');
                                        }
                                }
                        }).adopt(
                                new Element('img', { src: images.bitcoin, styles: { 'vertical-align': 'middle' } })
                        )
                ).inject(donationCell);
		new Element('label', {
			id: 'UCP.preferences_title',
			html: 'UNIFIED CheckPlay - Preferences for this group',
			styles: {
				fontWeight: 'bold',
				//width: (table.getStyle('maxWidth').match(/(\d+)/)[1] - 50) + 'px',
				'float': 'center',
				margin: 'auto'
			},
			events: {
				'version': function(evt) {
					this.set('html', 'UNIFIED CheckPlay v' + evt.version + ' - Preferences for this group');
				}
			}
		}).inject(header);
		getVersion('UCP.preferences_title', 'version');
		new Element('a', {
			html: 'help',
                        href: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623600133810/#comment72157623475618601',
			title: 'opens in new tab',
			target: '_blank',
			styles: {
				'float': 'right'
			}
		}).inject(header);
            // style
            new Element('tr').inject(table).adopt(
                new Element('th', {
                    colSpan: '3',
                    noWrap: 'nowrap',
                    vAling: 'top',
                    html: "Display:",
                    styles: {
                        background: '#CFCFCF'
                    }
                }));

            new Element('tr', { valign: 'top' }).inject(table).adopt(
                new Element('td', {
                    noWrap: 'nowrap',
                    vAling: 'top'
                }).adopt(
                    new Element('input', {
                        type: 'checkbox',
                        id: 'ucpstyleRadio',
                        checked: global_group_preferences.ucpStyle(),
                        events: {
			    change: function (evt) {
				if ($('ucpstyleRadio').checked) {
					topicListingTable.getElements('span.New').addClass('UCP_hidden_from_view');
					topicListingTable.getElements('li').addClass('UCP_row_hover');
					global_group_preferences.setStyle(true);
				} else {
					topicListingTable.getElements('span.New').removeClass('UCP_hidden_from_view');
					topicListingTable.getElements('li').removeClass('UCP_row_hover');
					global_group_preferences.setStyle(false);
				}
			    }
			}
                    }),
                    new Element('label', {
                        'for': "ucpstyleRadio",
                        html: "UCP style"
                    })),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "Unified CheckPlay style:"
                }).adopt(
		    new Element('p', {
			html: "UCheckPlay no longer provides customization of the Discuss page layout. There is a nice UserStyle created by 'loupiote (Old Skool)' that will compact the Discuss list to resemble the old style.<br>" +
				"It can be installed using Stylish, or it can be installed as a Greasemonkey (Firefox), or Tampermonkey(Chrome) user script.<br>" +
				"Install it from <a href='http://userstyles.org/styles/90360/flickr-show-group-discussions-in-compact-format?r=1375322249'>http://userstyles.org/styles/90360/flickr-show-group-discussions-in-compact-format?r=1375322249</a>.<br><br>" +
				"Selecting this option will only remove the 'New' labels, and color the row under your mouse."
		    }),
                    new Element('br'),
                    new Element('input', {
                        type: 'checkbox',
                        id: 'ucpStyleRemoveNew',
                        checked: true,
                        disabled: true
                    }),
                    new Element('label', {
                        'for': "ucpStyleRemoveNew",
                        html: "remove 'NEW' labels",
                        styles: {
                            color: 'grey'
                        }
                    }),
                    new Element('br'),
                    new Element('input', {
                        type: 'checkbox',
                        id: 'ucpStyleColorizeOnHover',
                        checked: true,
                        disabled: true
                    }),
                    new Element('label', {
                        'for': "ucpStyleColorizeOnHover",
                        html: "highlight on mouse hover",
                        styles: {
                            color: 'grey'
                        }
                    }),
                    new Element('br'),
		    new Element('span', { html: '&nbsp;' })
                )
            );
	    // check photo numbering
	    new Element('tr', { valign: 'top' }).inject(table).adopt(
		new Element('td', {
                    noWrap: 'nowrap',
                    vAling: 'top'
                }).adopt(
                    new Element('input', {
			type: 'checkbox',
			id: 'ucp_checkNumbering',
			checked: global_group_preferences.checkPhotoNumbering(),
			events: {
				click: function (evt) {
					$('ucp_checkNumberingGently').set('disabled', !this.checked);
					$('ucp_checkNumberingGentlyLabel').setStyle('color', this.checked ? '' : 'grey');
					global_group_preferences.setCheckPhotoNumbering($('ucp_checkNumbering').checked);
					$$('.UCP_status_column').dispose();
            				processTopicListingTable(topicListingTable);
				}
			}
		    }),
		    new Element('label', {
			'for': 'ucp_checkNumbering',
			html: 'check numbers'
		    })
		),
		new Element('td', {
        	        styles: {
                	        fontStyle: 'italic'
                        }
		}).adopt(
			new Element('label', {
				html: 'Check the numbering of entered photos (if applicable). When set, the script will check if the numbering' +
					' on the photos is correct. It will perform this check'
			}),
			new Element('div', { html: '&bull; for challenges with at least 4 photos' }),
			new Element('div', { html: '&bull; of which at least 2 have a number set' }),
			new Element('br'),
			new Element('p'),
			new Element('input', {
				type: 'checkbox',
				id: 'ucp_checkNumberingGently',
				disabled: !global_group_preferences.checkPhotoNumbering(),
				checked: global_group_preferences.checkPhotoNumberingGently(),
				events: {
					click: function (evt) {
						global_group_preferences.setCheckPhotoNumberingGently($('ucp_checkNumberingGently').checked);
					}
				}
			}),
			new Element('label', {
				'for': 'ucp_checkNumberingGently',
				id: 'ucp_checkNumberingGentlyLabel',
				html: 'be forgiving: allow for missing number on first, and last entry',
	                        styles: {
        	                    color: !global_group_preferences.checkPhotoNumbering() ? 'grey' : ''
                	        }
			})
		)
	    );
            // language
            new Element('tr').inject(table).adopt(
                new Element('th', {
                    colSpan: '3',
                    noWrap: 'nowrap',
                    vAling: 'top',
                    html: "Language:",
                    styles: {
                        background: '#CFCFCF'
                    }
                }));
            new Element('tr').inject(table).adopt(
                new Element('td').adopt(
                    languageSelect = new Element('select', {
                        styles: {
                            background: '#F0F0F0'
                        },
			disabled: global_group_preferences.useLegacyLabels(),
                        title: 'UCP language: select your preferred language for UCP feedback',
                        id: 'languageSelect',
			events: {
				change: function (evt) {
        				global_group_preferences.setLanguage($('languageSelect').value);
				}
			}
                    })),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "Language for titles, and labels."
                })
            );
	    new Element('tr').inject(table).adopt(new Element('td', { colspan: 2, styles: { position: 'relative', left: '100px' }, html: '<i>(needs page reload to take effect)</i><br>&nbsp;' }));
            ucpLanguageConfigReader.getLanguageList(function(retval) {
		if (!retval.success) {
			new Element('div', {
				html: retval.message,
				styles: {
					color: 'red'
				}
			}).replaces('languageSelect');
			return;
		}
		var languages = retval.languageList;
		$each(languages, function (definition, language) {
			    new Element('option', {
				value: language,
				selected: global_group_preferences.language() === language,
				disabled: definition === undefined || definition === null,
				title:  (definition === undefined || definition === null ? 
				    'Not implemented yet. Maybe you can help us out?' : 
				    language),
				html: language
			    }).inject($('languageSelect'));
		});
	    });
            // legacy labels
            if (global_group_config.hasLegacyLabels() && !global_group_config.mandatoryGroupLabels()) {
                new Element('tr').inject(table).adopt(
                    new Element('th', {
                        colSpan: '3',
                        html: "Legacy status icons:",
                        styles: {
                            background: '#CFCFCF'
                        }
                    }));
                new Element('tr', { valign: 'top' }).inject(table).adopt(
                    new Element('td', {
                        noWrap: 'nowrap'
                    }).adopt(
                        new Element('input', {
                            type: 'checkbox',
                            name: 'legacyicons',
                            id: 'legacyCheckId',
                            checked: global_group_preferences.useLegacyLabels(),
			    events: {
				click: function (evt) {
					// legacy lables are read when reading the group config: only on page load
					global_group_preferences.setUseLegacyLabels($('legacyCheckId').checked);
					$('languageSelect').set('disabled', $('legacyCheckId').checked);
				}
			    }
                        }),
                        new Element('label', {
                            'for': 'legacyCheckId',
                            html: 'Use legacy status icons',
			    styles: {
				'margin-right': '16px'
			    }
                        })),
                    new Element('td', {
                        styles: {
                            fontStyle: 'italic'
                        },
                        html: "Use the same status icons as the legacy CheckPlay script for this group"
                    }));
		new Element('tr').inject(table).adopt(new Element('td', { colspan: 2, styles: { position: 'relative', left: '100px' }, html: '<i>(needs page reload to take effect)</i><br>&nbsp;' }));
            }
            
            // group definitions
            new Element('tr').inject(table).adopt(
                new Element('th', {
                    colSpan: '3',
                    noWrap: 'nowrap',
                    vAling: 'top',
                    html: "Definitions:",
                    styles: {
                        background: '#CFCFCF'
                    }
                }));
            new Element('tr').inject(table).adopt(
                new Element('td', { noWrap: 'nowrap' }).adopt(
                    new Element('img', {
			src: images.download,
                        id: 'languageReload',
			title: 'click to update the script\'s language definitions',
			styles: {
				height: '12px',
				cursor: 'pointer'
			},
			events: {
				click: function (evt) {
					this.set('src', images.updating);
					GM_deleteValue(UCPprefix + ".languageConfig." + global_group_preferences.language()); // force an update
                                    	ucpLanguageConfigReader.checkForUpdates(global_group_preferences.language(), function (result) {
	                                        $('languageReload').set('src', result.success ? images.defaultCheck : images.error);
        	                                $('languageReload').set('title', result.success ? 'updated' : result.message);
						$('languageReloadLabel').set('title', result.success ? 'updated' : result.message);
                	                    });
				}
			}
                    }),
                    new Element('label', {
                        html: '&nbsp;language&nbsp;definitions',
			id: 'languageReloadLabel',
			title: 'click to update the script\'s language definitions',
			styles: {
				cursor: 'pointer',
				'margin-right': '16px'
			},
			events: {
				click: function (evt) {
					$('languageReload').fireEvent('click');
				}
			}
                    })
                ),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "The definitions for the language translations are updated once a month. With this option, you " +
                          "can update them now."
                })
            );
            new Element('tr').inject(table).adopt(
                new Element('td', { noWrap: 'nowrap' }).adopt(
                    new Element('img', {
			src: images.download,
                        id: 'ucp_groupReload',
			title: 'click to update the group\'s challenge definitions',
			styles: {
				height: '12px',
				cursor: 'pointer'
			},
			events: {
				click: function (evt) {
					this.set('src', images.updating);
        				GM_deleteValue(UCPprefix + ".groupConfig." + global_groupname); // force an update
					ucpGroupConfigReader.checkForUpdates({ group_name: global_groupname, callback: function (result) {
        	                                $('ucp_groupReload').set('src', result.success ? images.defaultCheck : images.error);
                	                        $('ucp_groupReload').set('title', result.success ? 'updated' : result.message);
						$('ucp_groupReloadLabel').set('title', result.success ? 'updated' : result.message);
                        	            }
					});
				}
			}
                    }),
                    new Element('label', {
                        html: '&nbsp;challenge&nbsp;definitions',
			title: 'click to update the group\'s challenge definitions',
			id: 'ucp_groupReloadLabel',
			styles: {
				cursor: 'pointer'
			},
			events: {
				click: function (evt) {
					$('ucp_groupReload').fireEvent('click');
				}
			}
                    })
                ),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "The definitions for the challenges are updated once a week. With this option, you " +
                          "can update them now."
                })
            );
            new Element('tr').inject(table).adopt(
                new Element('td', {
                    colSpan: '3',
                    align: 'right'
                }).adopt(

                    new Element('button', {
                        type: 'submit',
                        'class': 'DeleteButt',
                        html: 'Close',
                        events: {
                            click: togglePreferencesDialog
                        }
                    })
                ));

        }
    }

// -----

    function cleanupUCPvariables(group_name) {
    	if ($chk(group_name)) {
    		GM_listKeys(new RegExp("UCP." + group_name)).each(function (key) { GM_deleteValue(key); });
    	} else {
    		var keys = [];
    		GM_listKeys(null, function (key) {
    			if (key.match(/UCP./) && // for versions prior to 0.5.10
    					!key.match(/UCP.language/) &&
    					!key.match(/UCP.ucpStyle/) &&
    					!key.match(/UCP.useLegacyIcons/) &&
    					!key.match(/UCP.checkPhotoNumbering/) &&
    					!key.match(/UCP.checkPhotoNumberingGently/) &&
    					!key.match(/UCP.lastVersionCheckTime/) &&
    					!key.match(/UCP.onlineVersion/)) {
    				keys.push(key);
    			}
    		});
    		keys.each(function (key) { GM_deleteValue(key); });
    	}
    }

    var UCPMarkFunction = new Class({
        Implements: [Options],
        options: {
            markAnchor: undefined,
            imgNode: undefined,
            marked: false
        },
        initialize: function (options) {
            this.setOptions(options);
        },
        handleEvent: function (e) {
            if (this.options.marked) {
                this.options.marked = false;
                this.options.imgNode.set('class', 'UCPthumbnail_not_marked');
                this.options.markAnchor.set('html', this.options.markAnchor.get('html').replace("marked", "mark"));
                this.options.markAnchor.style.background = "yellow";
                this.options.markAnchor.title = "click to mark";
            } else {
                this.options.marked = true;
		this.options.imgNode.set('class', 'UCPthumbnail_marked');
                this.options.markAnchor.set('html', this.options.markAnchor.get('html').replace("mark", "marked"));
                this.options.markAnchor.set('style', 'background: magenta');
                this.options.markAnchor.title = "click to unmark";
            }
        }
    });
    
    function showPhotoSummary(photos) {
        var form = $$('table.TopicReply form textarea');
        if (!$chk(form) || form.length === 0) {
            GM_log("no form found (3)");
            return;
        }
        var helpmeButton = new Element('button', {
            'class': 'Butt',
            html: 'thumbnail view',
            events: {
                click: function () {
                    toggleThumbnailView(photos);
                }
            }
        }).inject(document.getElement('a[name=reply]').getParent(), 'before');
        new Element('a', {
            target: '_blank',
            html: 'what is this?',
            href: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623600133810/#comment72157623601636570',
            styles: {
                cursor: 'help'
            }
        }).inject(helpmeButton, 'after');
    }

GM_addStyle('.UCPthumbnail_not_marked { max-width: 75px; max-height: 75px; }');
GM_addStyle('.UCPthumbnail_marked { max-width: 120px; max-height: 100px; border: 7px solid magenta; }');
 
    function toggleThumbnailView(photos) {
        if ($chk($('UCP:thumbnail_table'))) {
            $('UCP:thumbnail_table').dispose();
            $$('table.helpmeanchor').dispose();
            $$('a[name=helpme]').dispose();
        } else {
            var main = document.getElement('table.TopicReply');
            var photoTableAnchor = new Element('a', {
                name: "helpme"
            });
            photoTableAnchor.inject(main, 'after');

            var photoTable = new Element('table', { id: "UCP:thumbnail_table" });
            photoTable.inject(photoTableAnchor, 'after');

            photoTable.adopt(
                new Element('tr').adopt(
                    new Element('th', {
                        colSpan: 5,
                        styles: {
                            color: "brown"
                        },
                        html:
                            "Please view the photos in medium size first.<br/>" +
                            "You will find a 'mark' link below every photo. " +
                                                   "Click the link to mark the photo's thumbnail below.<br/>" +
                            "This should make it easier to narrow down your selection."
                    }),
                    new Element('th', {
                        colSpan: 5,
                        styles: {
                            color: 'brown',
                            'text-align': 'right'
                        },
                        html:
                            "Double-check the numbers please!<br/>" +
                            "!! And don't forget to enter your vote !!"
                    })
                )
            );
            var markAnchorList = [];
            photos.each(function (competitor, photoIdx) {
                if (photoIdx % 9 === 0) {
                    photoTable.adopt(row = new Element('tr', { valign: 'bottom' }));
                }
                var cell;
                row.adopt(cell = new Element('td', {
                    html: photoIdx + 1,
		    styles: {
			    'white-space': 'nowrap',
			    'border-style': 'solid',
			    'border-color': 'grey',
			    'border-width': '1px;'
		    }
                }));

                var photoAnchor;
                cell.adopt(photoAnchor = new Element('a', {
                        href: '#UCP_thumbnail_hook_' + competitor.photoId()
                }));
                // trick the rest of the script
                cell = photoAnchor;
                var thumb;
                cell.adopt(thumb = new Element('img', {
                    src: competitor.photo().src,
                    alt: 'UCPthumbnail',
		    'class': 'UCPthumbnail_not_marked'
                }));
                // fetch the thumbnail version from Flickr? will not work for photos with all rights reserved
                // => no 'All sizes', no public thumbnail
		// heck, the medium sized version should be in the browser's cache anyhow
                var markAnchor = { 
                    anchor: new Element('a', {
                        html: "mark",
                        styles: {
                            background: "yellow",
                            cursor: 'pointer'
                        },
                        title: "click to mark",
                        'class': 'helpmeanchor'
                    }),
		    photoAnchor: new Element('a', { name: 'UCP_thumbnail_hook_' + competitor.photoId(), html: '&nbsp;' }),
                    photo: competitor.photo(),
		    photoId: competitor.photoId()
                };
                //markAnchor.anchor.style.textDecoration = "underline";
                var markFunction = new UCPMarkFunction({
                    markAnchor: markAnchor.anchor,
                    imgNode: thumb
                });
                markAnchor.anchor.addEventListener('click', markFunction, false);
                markAnchorList.push(markAnchor);
            });

            markAnchorList.each(function (markAnchor) {
                var div = new Element('div', { width: '100%', 'class': 'helpmeanchor' }).adopt(
                    new Element('table', {
                        'class': 'helpmeanchor',
                        width: '100%'
                    }).adopt(
                        new Element('rw').adopt(
                            new Element('td').adopt(markAnchor.anchor),
                            new Element('td', {
                                align: 'right',
                                width: '100%'
                            }).adopt(
                                new Element('a', {
                                    'class': 'toThumbnails',
                                    href: '#helpme',
                                    html: 'to the thumbnails',
                                })
                            )
                        )
                    )
                );
		var pagePhotoNode = $$('img[src*=' + markAnchor.photoId + ']')[0];
                if ($chk(pagePhotoNode.getParent('a'))) {
                    div.inject(pagePhotoNode.getParent('a'), 'after');
		    markAnchor.photoAnchor.inject(pagePhotoNode.getParent('a'), 'before');
                } else {
                    // hope for the best
                    div.inject(pagePhotoNode, 'after');
		    markAnchor.photoAnchor.inject(pagePhotoNode, 'before');
                }
            });
            document.location.href = "#helpme";
        }
    }

    var threadPage = false;
    function processDiscussionTopic(discussionTopic, ucpThread) {
        if (!$chk(discussionTopic)) {
            GM_log("no discussion topic?");
            ucpThread.setChallengeStatus("ERRORPARSING");
            ucpThread.addVotingError("no DiscussTopic");
            return;
        }
	var playerInThisTopic = false;
	var can_post = discussionTopic.topic.is_locked != 1 && discussionTopic.topic.is_locked != '1' && (discussionTopic.topic.can_reply == 1 || discussionTopic.topic.can_reply == '1');
	if (!can_post) { // closed, or not a member
		// mark it as closed
		ucpThread.resetStatus();
		ucpThread.setChallengeStatus("Closed");
		if (global_group_adminOrMod) {
			ucpThread.printStatus(global_group_preferences, global_ucp_language);
		}
	}
	// add UCPNGFormFeedback
	if (ucpThread.options.needsStatusOnChallengePage) {
		var form = $(document).getElement('table.TopicReply form textarea');
		if (!$chk(form)) { // closed, or not a member
		    form = $(document).getElement('table.TopicReply div.Tease');
		    if (!$chk(form) || form.length === 0) {
			GM_log("no form found");
			ucpThread.resetStatus();
			ucpThread.setChallengeStatus("Closed");
			if (global_group_adminOrMod) {
			    ucpThread.printStatus(global_group_preferences, global_ucp_language);
			}
		    }
		}
		if ($chk(form)) {
		    var formFeedback = new Element('small', {
			id: 'UCPNGFormFeedback',
			styles: {
			    textDecoration: 'none'
			}
		    }).inject(form, 'before');
		    ucpThread.setFeedbackElement(formFeedback);
		} else {
			if (global_group_adminOrMod) {
			    var closedMessage = $('main').getElement('p.Focus');
			    if ($chk(closedMessage)) {
				//GM_log("found the Closed message");
				var formFeedback = new Element('small', {
				    id: 'UCPNGFormFeedback',
				    styles: {
					textDecoration: 'none'
				    }
				}).inject(closedMessage, 'before');
				ucpThread.setFeedbackElement(formFeedback);
			    } else {
				GM_log("no lock found");
			    }
			}
		}
	}
	
        if (ucpThread.challengeDefinition().nonChallengeType()) {
            ucpThread.resetStatus();
            ucpThread.printStatus(global_group_preferences, global_ucp_language);
            return;
        }
	var challengeAnnouncement = discussionTopic.topic;
        // the announcement could contain override information:
        // <img src="..." alt="UCPoverride:photos:3"/>
        // <img src="..." alt="UCPoverride:votes:10"/>

	var challengeAnnouncementDiv = new Element('div', { html: challengeAnnouncement.message._content });
        ucpThread.challengeDefinition().readChallengeDefinitionOverrides(challengeAnnouncementDiv);
        // re-test !!
        if (ucpThread.challengeDefinition().nonChallengeType()) {
            ucpThread.resetStatus();
            ucpThread.printStatus(global_group_preferences, global_ucp_language);
            return;
        }

        if (ucpThread.challengeStatus() !== "Closed") { // in case of admins, continue
            ucpThread.setChallengeStatus("none");
        }

        if (debug) GM_log("ucpThread: " + ucpThread.toString());
        if (debug) GM_log("challenge: " + ucpThread.challengeDefinition().toString());

	var excluded = false;
        var replies = discussionTopic.reply;
        if (global_group_config.excludeReplyIndexes().contains(0)) {
            ucpThread.findExcludesInText(challengeAnnouncement.message._content);
            if (ucpThread.isExcluded(username)) {
		excluded = true;
                ucpThread.updateStatus("Excluded");
            }
        } else if (debug) {
        	GM_log("no 0 index exclude");
        }
	
        if (!$chk(replies) || replies.length === 0) {
	    if (!excluded) {
	            ucpThread.resetStatus();
        	    ucpThread.setChallengeStatus("none");
	    }
            ucpThread.checkStatus();
            ucpThread.printStatus(global_group_preferences, global_ucp_language);
	    ucpThread.printExcludes();
            return;
        }
	
        global_group_config.excludeReplyIndexes().each(function (index) {
        	// in versions prior to 3.0.3, 'replies' contained the announcement => 0 was announcement
        	var challengeEntry = replies[index - 1];
        	if (challengeEntry && index > 0) { // already done the announcement
        		ucpThread.findExcludesInText(challengeEntry.message._content);
        		if (ucpThread.isExcluded(username)) {
        			ucpThread.updateStatus("Excluded");
        		}
        	}
        });
        ucpThread.printExcludes();
	
        ucpThread.collectVotes(challengeAnnouncement, replies, function () {
		var votes  = ucpThread.votes();
		var comments = ucpThread.comments();
		var photos = ucpThread.photos();
		
		var doubleVotesHash = new Hash();
		var doublePlayerHash = new Hash();

		var photosNeedNumbering = global_group_preferences.checkPhotoNumbering() ? photos.length >= 4 && photos.filter(function (photo) { return $chk(photo.photoNumber()); }).length > 1 : false;
		//GM_log("DEBUG: need numbering='" + photosNeedNumbering + "'");
		var photoNumber = 1;

		$each(photos, function (photo) {
		    var photoposter = photo.poster().username;
		    if (photoposter === username) {
			ucpThread.updateStatus("photoposter");
			if (ucpThread.challengeDefinition().countsToLimit() && !playerInThisTopic) { // only count once
			    ++playernumber;
			    playerInThisTopic = true;
			}
		    }
		    if (ucpThread.challengeDefinition().limitPerPlayer() > 0) {
			if (doublePlayerHash.has(photo.poster().userid)) {
			    // this member has more than one photo entered
			    var entries = doublePlayerHash.get(photo.poster().userid) + 1;
			    if (entries > ucpThread.challengeDefinition().limitPerPlayer()) {
				if (!photo.poster().admin) {
					if (global_group_adminOrMod) { // only show the error to admods
					    ucpThread.addVotingError("'" + photo.poster().username + "' entered more than " + 
							    ucpThread.challengeDefinition().limitPerPlayer() + " photo(s)");
					}
				}
				photo.addError("(max. photos allowed: " + ucpThread.challengeDefinition().limitPerPlayer() + ")");
			    }
			    doublePlayerHash.set(photo.poster().userid, entries);
			} else {
			    doublePlayerHash.set(photo.poster().userid, 1);
			}
		    }
		    //GM_log("comparing '"+excludedPlayer+"' with '"+vote.username+"'");
		    if (ucpThread.excludedPlayers().contains(photo.poster().username)) {
			ucpThread.addVotingError("'" + photo.poster().username + "' was excluded, but entered anyway");
			photo.addError(photo.poster().username + " was excluded, but entered anyway");
		    }
		    if (photosNeedNumbering) {
			if (!$chk(photo.photoNumber())) {
				if ((photoNumber == 1 || photoNumber == photos.length) && global_group_preferences.checkPhotoNumberingGently()) {
					// skip
				} else {
					photo.addWarning("did not number his/her entry");
					ucpThread.addVotingError("'" + photo.poster().username + "' did not number his/her entry");
				}
			} else if (photo.photoNumber() != photoNumber) {
				photo.addWarning("numbered incorrectly (" + photo.photoNumber() + " should be " + photoNumber + ")");
				ucpThread.addVotingError("'" + photo.poster().username + "' numbered his/her entry incorrectly");
			}
			++photoNumber;
		    }
		    photo.printStatus();
		}); // photo loop

		// non-vote comments
		$each(comments, function (comment) {
		    comment.ucpThread = ucpThread;
		    if (ucpThread.challengeDefinition().scoreType() === "MEETANDGREET") {
			if (comment.poster().username === username) {
			    ucpThread.updateStatus("voter");
			}
		    }
		    comment.printStatus();
		});

		// votes
		var cummulativeScore = new UCPVote({
		    chlgname: null, 
		    poster: "flickr", 
		    voteText: "none", 
		    votesArray: [], 
		    commentAnchor: null,
		    //ucpThread: ucpThread,
		    node: null,
		    messages: [],
		    errorIdx: null,
		    votedFor: []
		});
		cummulativeScore.options.ucpThread = ucpThread; // if given with the above constructor, errors out with 'too much recursion'

		var previousVote = undefined;
		$each(votes, function (vote, j) {
		    if (vote.isVoid()) {
			vote.addMessage("found a voided vote");
			vote.printStatus();
			return;
		    }
		    var theFirstAndExampleVote = previousVote === undefined && vote.isExampleVote();
		    if (theFirstAndExampleVote) {
			vote.addMessage("found an example vote");
			vote.printStatus();
		    } else {
			if (ucpThread.finished(cummulativeScore)) {
				vote.addError("voted after voting had finished");
				ucpThread.addVotingError("'" + vote.poster().username + "' voted after voting had finished");
			}
			vote.addMessage(
				"<b>" + 
				(
				    vote.poster().username === username ? 
				    "you" : 
				    vote.poster().username
				) + 
				(
				    vote.poster().admin ?
				    " (admin/mod)" : ""
				) +
				"</b> voted");
			if (doubleVotesHash.has(vote.poster().userid)) { // this member already voted!
			    vote.addError("voted more than once");
			    ucpThread.addVotingError("'" + vote.poster().username + "' voted more than once");
			} else {
			    doubleVotesHash.set(vote.poster().userid, vote.poster().username); // whatever
			}
			if (vote.poster().username === username) {
			    ucpThread.updateStatus("voter");
			}
			if (ucpThread.challengeDefinition().scoreType().match(/PIC-[PHV]-/) ||
			    ucpThread.challengeDefinition().scoreType().match(/RATE-PHOTO/)) {

			    cummulativeScore.add(vote);
			} else { // HORIZONTAL, VERTICAL, VERTICAL-WEIGHTED
			    cummulativeScore = vote; // last vote contains the full score
			}
			if (ucpThread.finished(cummulativeScore)) {
			    ucpThread.updateStatus("finished");
			}
		    }
		    var currentValidVoting = vote.valid(previousVote, photos.length, j); // use the previousVote, even if it is an example
		    if (!theFirstAndExampleVote) {
			var votedForString = vote.votedFor();
			if (votedForString.length > 0) {
			    vote.addMessage("for photo" + (votedForString.match(/,/) ? "s " : " ") + votedForString);
			}
			if (!currentValidVoting && !ucpThread.challengeDefinition().scoreType().match(/PIC-P/)) {
			    ucpThread.addVotingError("'" + vote.poster().username + "' " + vote.error());
			    if (!ucpThread.validVoting()) { // already in error from previous vote
				vote.messages().each(function (message) {
				    message.type = 'warning';
				});
			    }
			}
			// any PIC type should not show the intermediate result
			if (!ucpThread.challengeDefinition().scoreType().match(/PIC-[HV]/) &&
			    !ucpThread.challengeDefinition().scoreType().match(/RATE-PHOTO/)) {
			    var votingResult = ucpThread.challengeDefinition().scoreType().match(/PIC-P/) ? 
				cummulativeScore.showPicResult() : cummulativeScore.showVotes();
			    vote.addMessage(votingResult);
			}
			if (ucpThread.challengeDefinition().playerVoting() === "maynotvote") {
			    if (doublePlayerHash.has(vote.poster().userid)) {
				ucpThread.addVotingError(
				     "'" + vote.poster().username + "' voted in a challenge he/she plays in");
				vote.addError("in a challenge he/she plays in");
			    }
			}
			vote.printStatus();
		    }
		    if (ucpThread.challengeDefinition().scoreType().match(/PIC-P/)) {
			previousVote = cummulativeScore;
			previousVote.options.poster = vote.options.poster;
		    } else {
			previousVote = vote;
		    }
		});
		previousVote = undefined;

		//overwrite some base statusses if challenge is in voting.
		if (ucpThread.challengeName().match(global_group_config.states().closed)) { // should never happen
		    ucpThread.updateStatus("closed");
		}
		if (ucpThread.finished(cummulativeScore)) {
		    ucpThread.updateStatus("finished");
		}
		ucpThread.checkStatus();
		
		if (ucpThread.challengeStatus() === "Finished"    || ucpThread.challengeStatus() === "PlayerFinished" || 
		    ucpThread.challengeStatus() === "PlayerVoted" || ucpThread.challengeStatus() === "Voted" ||
		    ucpThread.challengeStatus() === "Closed" || ucpThread.challengeStatus() === "Player") {
		    if (ucpThread.challengeDefinition().scoreType().match(/PIC-P/)) {
			ucpThread.setScoreSummary(cummulativeScore.showPicResult(true));
		    } else {
			ucpThread.setScoreSummary(cummulativeScore.showVotes(true));
		    }
		}
		if (threadPage && !(ucpThread instanceof UCPUnknownThread)) {
		    if (ucpThread.challengeName().match(/kanchenjunga/i)) {
			photos = ucpThread.findPhotos({ entry: challengeAnnouncement, all: true });
		    } // photos in Kanchenjunga are all in the challenge header
		    if (ucpThread.challengeDefinition().thumbnailView() == true) {
			showPhotoSummary(photos);
		    }
		}
		//let's go and change the update status on screen
		   
		//update thread info
		ucpThread.printStatus(global_group_preferences, global_ucp_language);
	});
    }

    function processTopicListingTable(topicListing_table) {

    // select main table
        //var topicListingHeaderRow = document.evaluate(".//tr", topicListingTable, null, 
        //                XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
	var statusTitleCell = $('UCP_preferences_span');
        var statusTitleDiv = new Element('span', {
            styles: {
                textAlign: 'center'
            }
        }).adopt(new Element('a', {
            html: "[ documentation ]",
            target: '_blank',
            href: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623600133810/#comment72157623600157950',
            styles: {
                cursor: 'help',
		'margin-left': '5px',
		'margin-right': '5px'
            }
        }));

        statusTitleDiv.inject(statusTitleCell, 'after');
        
	// prepare for dynamic ucpStyle changes

	topicListing_table.getElements('li').each(function (topicRow) {
		new Element('div', { 'class': 'UCP_status_column' }).inject(topicRow.getElement('div.message div.ft'));
	});
        if (global_group_preferences.ucpStyle()) {
		// hide 'New' labels (do not remove: could break other scripts)
		topicListing_table.getElements('span.New').addClass('UCP_hidden_from_view');
		topicListing_table.getElements('li').addClass('UCP_row_hover');
	}
        var topicListingRows = topicListing_table.getElements('li');
        // let's loop the table and start processing
        topicListingRows.each(function (topicRow, i) {
            var challengeColumn = topicRow.getElement('div.hd');
            var topic = challengeColumn.getElement('a');
            var chlgname = topic.textContent;
            var ucpThread = ucpCreateChallengeThread({
                chlgAnchor: topic,
                url: topic.href,
                chlgname: chlgname,
                groupConfig: global_group_config,
                needsStatusOnTopicListing: true
            });
            //debug = chlgname.match('Naveesh');
            // reset
            var skipChallenge = global_group_config.skipChallenge(ucpThread.challengeName());

            var counterColumn = topicRow.getElement('div.UCP_status_column');

            // add statusses
	    myanchor = new Element('a', {
		id: "UCPNG." + topic.href,
		href: topic.href
	    });
	    statusCell = myanchor;
            statusCell.inject(counterColumn, 'bottom');
            ucpThread.setLabelElement(myanchor);
            ucpThread.setScoreAnchor(counterColumn);
            if (!skipChallenge) // not "---"
            {
                ucpThread.setChallengeStatus("none"); // remove VOTE or FINISHED
                 // in case the name changed, make sure the rest of the script uses the new name
                ucpThread.setChallengeName(chlgname);
                ucpThread.printStatus(global_group_preferences, global_ucp_language, "UPDATING");
                ucpThread.resetStatus();
		ucpThread.loadthread(function (result) {
			var success = result.success;
			var message = result.message;
			var discussionTopic = result.discussionTopic;
			var ucpThread = result.ucpThread;
			if (!success) {
				// TODO: use msg as popup title?
				ucpThread.printStatus(global_group_preferences, global_ucp_language);
			} else {
				processDiscussionTopic(discussionTopic, ucpThread);
			}
		});
            } else { // "---"
                // differentiate between two types '---':
                // closed vs. unknown
                ucpThread.resetStatus();
                ucpThread.printStatus(global_group_preferences, global_ucp_language);
            }

        });
        //GM_log("End of processing main discuss page");

        return;
    }

// *******************
// Start of processing
// *******************

    if (window.name === 'Log page') {
        throw new Error('not processing log page');
    }

	new Element('span', {
		id: 'UCP-updatenotifier',
		'class': 'UCP_hidden_from_view',
		events: {
			'checkversion': function (evt) {
				checkVersion(evt.version);

				// if version is newer than stored one, clear all GM variables
				if (!$chk(GM_getValue("UCP.version"))) {
					cleanupUCPvariables();
				} else {
					var storedVersion = GM_getValue("UCP.version");
					if (evt.version !== storedVersion) {
						cleanupUCPvariables();
					}
				}
				GM_setValue("UCP.version", evt.version);
			}
		}
	}).inject($$('body')[0], 'bottom');
	getVersion('UCP-updatenotifier', 'checkversion');

// check if we have GM variables
    if (!$chk(GM_getValue("UCP.lastloadtime." + global_groupname))) {
        GM_setValue("UCP.lastloadtime." + global_groupname, Math.round(CPStartTime.getTime() / 1000));
    }

    // 
    // read UCP.http://www.flickr.com/groups/name/discuss/.lastloadtime
    var lastloadtime = parseInt(GM_getValue("UCP.lastloadtime." + global_groupname), 10) * 1000;
    var elapstime = CPStartTime.getTime() - lastloadtime;

    if (elapstime > 1000 * 60 * 60) //more than 1 hour : cleanup
    {
        // clear all GM_values for this group
        cleanupUCPvariables(global_groupname);
    }
    // store UCP.http://www.flickr.com/groups/name/discuss/.lastloadtime
    GM_setValue("UCP.lastloadtime." + global_groupname, Math.round(CPStartTime.getTime() / 1000));

    var groupNavigationMenu = null;
    var navigation = null;
    if (document.location.href.match(/.*\/edit\/?/)) {
        throw new Error("not processing edit page");
    }
    
    if (document.location.href.match(/.*flickr.com\/groups\/.*\/discuss(?:\/page\d+)?(?:\/)?$/)) {
        GM_log("on discuss page");
        groupNavigationMenu = $('cattington_outer');
	if (!$chk(groupNavigationMenu)) {
		groupNavigationMenu = document.getElement('div.groups-header');
	}
        navigation = groupNavigationMenu;
        initialize(discussPageFunction);
    } else if (document.location.href.match(/.*flickr.com\/groups\/.*\/discuss\/\d+/)) { // can be followed with '/', '/page', '/#comment', '/#reply', ...
        GM_log("on challenge thread page");
        threadPage = true;
        navigation = $$('div#Main h1#Tertiary')[0];
	if (!$chk(navigation)) {
		navigation = document.getElement('div.main div.group-topic-detail-col');
	}
        initialize(threadPageFunction);
    } else if (document.location.href.match(/.*flickr.com\/groups\/.*\/admin\/pending\//)) {
        GM_log("on pending stuff page");
        navigation = $$('div#Main table#SubNav tbody tr').getElement('td.Section');
        initialize(pendingItemsPageFunction);
    } else {
        throw new Error("not on a supported group (or page); aborting");
    }

    function discussPageFunction(data) {
    	if (!data.success) {
		GM_log("aborting: " + data.message);
		throw new Error("aborting: " + data.message);
	}
    
        // only append [preferences] on Discuss page
        groupNavigationMenu.adopt(
        new Element('p', {
            'class': "LinksNewP",
            id: 'UCheckPlayNGPreferences'
        })).adopt(
            new Element('span', {
		id: 'UCP_preferences_span',
                styles: {
                    fontWeight: 'bold'
                },
                html: "UCheckPlayNG preferences:",
		events: {
			'showversion': function (evt) {
				this.set('html', this.get('html').replace(/:$/, ' ' + evt.version + ':'));
			}
		}
            })).adopt(
            new Element('a', {
                name: '#',
                title: "Click to change preferences",
                styles: {
                    color: "Blue",
                    cursor: "pointer",
		    'margin-left': '5px',
		    'margin-right': '5px'
                },
                html: "[ " +
                      (global_group_preferences.ucpStyle() ? "UCP style"  : "Flickr style") + " - " +
                      (global_group_preferences.language() ? global_group_preferences.language() : "English")
                      + " ]",
                events: {
                    click: togglePreferencesDialog
                }
            }),
            new Element('a', {
                html: "[ Supported Challenge Groups ]",
                name: "#",
                title: "Click to access the list of supported challenge groups",
                styles: {
                    color: "Blue",
                    cursor: "pointer",
		    'margin-left': '5px',
		    'margin-right': '5px'
                },
                events: {
                    click: toggleGroupList
                }
            })
        );
	getVersion('UCP_preferences_span', 'showversion');
        
	if (topicListingTable) {
            processTopicListingTable(topicListingTable);
        } else {
            GM_log("not processing (no TopicListing found!)");
        }

    }
        // create field for messages (reached limit, not logged in, ..)
    function reportStatus(str, type) {
	var statusDiv = $('UCheckPlayNGStatusDiv');
	var statusP = $('UCheckPlayNGStatus');

	if (!$chk(statusDiv)) {
		statusP = new Element('p', {
			id: "UCheckPlayNGStatus",
			styles: {
			    textDecoration: 'none',
			    color: 'Red'
			}
		});
		statusDiv = new Element('div', {
			id: "UCheckPlayNGStatusDiv"
		}).adopt(statusP);
		navigation.adopt(statusDiv);
	}
	statusP.set('html', str);
	if (type == 'error') {
		statusP.setStyles({ 'text-decoration': 'underline', 'font-weight': 'bold' });
	}
    }

    function threadPageFunction(data) {
    	if (!data.success) {
		throw new Error("error initializing: " + data.message);
	}
	var dummy = new UCPThread({ url: document.location.href });
	dummy.loadthread(function (result) {
		if (!result.success) {
			reportStatus("UCheckPlayNG: error loading page: " + result.message);
			return;
		}
		var discussionTopic = result.discussionTopic;
		// create a real UCPThread
		var ucpThread = ucpCreateChallengeThread({
			url: document.location.href,
			chlgname: discussionTopic.topic.subject,
			groupConfig: global_group_config,
			needsStatusOnChallengePage: true
		});
		if (!(ucpThread instanceof UCPChallengeThread)) {
			return;
		}
		var page = document.location.href.match(/\/page(\d+)/);
		if (page) {
			page = parseInt(page[1], 10);
		}
		if (page && page > 1) {
			reportStatus("UCheckPlayNG: no UCP checking on page " + page, 'error');
			return;
		}
		processDiscussionTopic(discussionTopic, ucpThread);
	});
    }
    
    function pendingItemsPageFunction (data) {
    	if (!data.success) {
		GM_log("aborting: " + data.message);
		throw new Error("aborting: " + data.message);
	}
    // nothing to do: for the admin script
    }

    // check library updates
    ucpGroupConfigReader.checkForUpdates({ group_name: global_groupname, callback: function (retval) {
		if (!retval.success) {
			GM_log("error updating group config: " + retval.message);
		}
	    }
    });

    // *******************
    //  End of processing
    // *******************

